JavaFXで印刷

年賀状印刷をMS Word+MS Excelの組み合わせでやっていたが、毎年印刷方法に泣かされるので簡単な自前アプリで出来ないものかと、まずは試してみようと思う。
手近な印刷用SDKとしては.NETかJavaがあったが、迷わずJavaで試す。
Javaには古くからあるAWTやJava Print Serviceなどがあるが、それらではなくJavaFX 8から追加になったJavaFX printing APIを使う。

サンプルアプリ

簡単なJavaFXのUIを使っていくつかのAPIを試して行く。

以下のようなサンプルアプリを作った。
f:id:nave_kazu:20170106185642p:plain

上のツールバーAPIを呼び出す各種ボタンと、そのツールバーの右側に部数を入力するエリア。
下の「Log area」にAPIを呼び出した結果をテキストで出力するエリア。
真ん中にアンカーペインを置いて、その中にラベルで「Hello JavaFX Print.」を表示している。
今回はこのアンカーペインを印刷する。

Scene Builderで見たHierarchyは下記の通り。
f:id:nave_kazu:20170106185651p:plain

画面の表示方法やイベント処理方法は今回の範囲外なので適当に。
まずはここまでのソースとFXMLの内容は下記の通り。

まずはFXML。
パスは「/fxml/Main.fxml」

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

<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="500.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="tools.javafx_printsample.MainController">
   <top>
      <ToolBar prefHeight="40.0" prefWidth="200.0" BorderPane.alignment="CENTER">
        <items>
          <Button fx:id="defaultPrinterButton" mnemonicParsing="false" onAction="#handleDefaultPrinterButtonAction" text="DefaultPrinter" />
            <Button fx:id="allPrintersButton" mnemonicParsing="false" onAction="#handleAllPrintersButtonAction" text="AllPrinters" />
            <Button fx:id="printButton" mnemonicParsing="false" onAction="#handlePrintButtonAction" text="Print" />
            <Separator orientation="VERTICAL" />
            <Label text="Copies" />
            <TextField fx:id="copiesField" alignment="CENTER_RIGHT" prefWidth="50.0" text="1" />
        </items>
      </ToolBar>
   </top>
   <bottom>
      <TextArea fx:id="logArea" prefHeight="100.0" promptText="Log area" BorderPane.alignment="CENTER" />
   </bottom>
   <center>
      <AnchorPane fx:id="printCanvas" BorderPane.alignment="CENTER">
         <children>
            <Label text="Hello JavaFX Print." />
         </children>
      </AnchorPane>
   </center>
</BorderPane>


続いてmain関数があるApp.java

package tools.javafx_printsample;

import java.io.IOException;

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

public class App extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws IOException {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/Main.fxml"));
        loader.load();
        Parent root = loader.getRoot();
        Scene scene = new Scene(root);
        primaryStage.setTitle("Print sample");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

}


最後に画面のコントローラ。

package tools.javafx_printsample;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;

public class MainController {
    @FXML Button defaultPrinterButton;
    @FXML Button allPrintersButton;
    @FXML Button printButton;
    @FXML TextField copiesField;
    @FXML AnchorPane printCanvas;
    @FXML TextArea logArea;

    @FXML
    public void handleDefaultPrinterButtonAction(ActionEvent event) {
    }

    @FXML
    public void handleAllPrintersButtonAction(ActionEvent event) {
    }

    @FXML
    public void handlePrintButtonAction(ActionEvent event) {
    }

    private void addLog(String text) {
        logArea.appendText(text+"\n");
    }
}

一番下の addLog メソッドはログエリアに引数の文字列と改行を追記する "手抜き" ユーティリティメソッド。

ここまでは下準備で、以降ではこのアプリに印刷用の処理を入れて行く。

プリンタの選択

印刷をするには印刷を実行させたいプリンタを選択しないとならない。
まずはプリンタ情報の取得を試してみる。

JavaFXの印刷APIは、パッケージで言うと「javafx.print」に入っている。
このパッケージのクラスを見ると、そのものずばりな「Printer」クラスがあり、プリンタの情報はこのクラスを通じて取得できる。

Printerクラスから各種情報を取得してログエリアに出力するメソッドをMainControllerに用意。

    private void outputPrinterInformation(Printer printer) {
        addLog("*** Name:"+printer.getName());

        // ページ・レイアウトの情報を取得して出力
        PageLayout pageLayout = printer.getDefaultPageLayout();
        PageOrientation pageOrientation = pageLayout.getPageOrientation();
        addLog("    PageLayout:");
        addLog("        TopMargin:"+pageLayout.getTopMargin());
        addLog("        LeftMargin:"+pageLayout.getLeftMargin());
        addLog("        RightMargin:"+pageLayout.getRightMargin());
        addLog("        BottomMargin:"+pageLayout.getBottomMargin());
        addLog("        PrintableHeight:"+pageLayout.getPrintableHeight());
        addLog("        PrintableWidth:"+pageLayout.getPrintableWidth());
        addLog("        PageOrientation:"+pageOrientation.name());

        // 用紙の情報を取得して出力
        Paper paper = pageLayout.getPaper();
        addLog("        Paper:");
        addLog("            Name:"+paper.getName());
        addLog("            Height:"+paper.getHeight());
        addLog("            Width:"+paper.getWidth());
    }

デフォルトプリンタの取得

Windowsで言う「通常使うプリンター」を取得するには、Printerクラスのスタティックメソッド getDefaultPrinter() を使う。
このアプリを試したWindows PCのプリンタ設定は下記のようになっている。
f:id:nave_kazu:20170106185657p:plain

3台登録されていて、「通常使うプリンター」にはPDF出力用のCubePDF、あとはWindows7標準のFAXとXPS Writerという構成。

「DefaultPrinter」ボタンをクリックした時のイベントハンドラに対して、getDefaultPrinter() の結果を outputPrinterInformation() に渡す処理を記述する。

    @FXML
    public void handleDefaultPrinterButtonAction(ActionEvent event) {
        Printer defaultPrinter = Printer.getDefaultPrinter();
        outputPrinterInformation(defaultPrinter);
    }


アプリを起動して実行し、「DefaultPrinter」ボタンをクリックした時の実行結果は下記の通り。

*** Name:CubePDF
    PageLayout:
        TopMargin:54.0
        LeftMargin:54.0
        RightMargin:54.0
        BottomMargin:54.0
        PrintableHeight:734.0
        PrintableWidth:487.0
        PageOrientation:PORTRAIT
        Paper:
            Name:A4
            Height:842.0
            Width:595.0

プリンタの名前が「CubePDF」で、四方のマージンの情報や、用紙の情報が出力された。
現在「通常使うプリンター」に設定しているのは「CubePDF」なので、意図した結果が得られた。

ちなみに数値の単位は「ポイント」で、これはDTPの用語。
1ポイントは「1/72 インチ」なので、上のA4用紙の高さ「842.0」は
842/72で約「11.69 インチ」。1インチは25.4mmなので、mmにすると「297mm」。
WikipediaによるとA4用紙のサイズは「210mm×297mm」だそうなので、842ポイントは297mmで正しいというのが分かる。

プリンタの検索

現在インストールされているプリンタ全ての情報を取得するには、Printerクラスのスタティックメソッド getAllPrinters() を使う。

「AllPrinters」ボタンをクリックした時のイベントハンドラに対して、getAllPrinters() の結果を outputPrinterInformation() に渡す処理を記述する。

    @FXML
    public void handleAllPrintersButtonAction(ActionEvent event) {
        Collection<Printer> collection = Printer.getAllPrinters();
        collection.stream().forEach(this::outputPrinterInformation);
    }

アプリを起動して実行し、「AllPrinters」ボタンをクリックした時の実行結果は下記の通り。

*** Name:CubePDF
    PageLayout:
        TopMargin:54.0
        LeftMargin:54.0
        RightMargin:54.0
        BottomMargin:54.0
        PrintableHeight:734.0
        PrintableWidth:487.0
        PageOrientation:PORTRAIT
        Paper:
            Name:A4
            Height:842.0
            Width:595.0
*** Name:Fax
    PageLayout:
        TopMargin:54.0
        LeftMargin:54.0
        RightMargin:54.0
        BottomMargin:54.0
        PrintableHeight:684.0
        PrintableWidth:504.0
        PageOrientation:PORTRAIT
        Paper:
            Name:Letter
            Height:792.0
            Width:612.0
*** Name:Microsoft XPS Document Writer
    PageLayout:
        TopMargin:54.0
        LeftMargin:54.0
        RightMargin:54.0
        BottomMargin:54.0
        PrintableHeight:734.0
        PrintableWidth:487.0
        PageOrientation:PORTRAIT
        Paper:
            Name:A4
            Height:842.0
            Width:595.0

CubePDF、Fax、XPS Writerの3つ全ての情報が得られた。

プリンタの情報が得られたのは分かったので、実際に印刷を行なってみる。

出力方法の設定

印刷の単位は「印刷ジョブ」と呼ばれる。
javafx.print」パッケージを見ると、これまたドンピシャな「PrinterJob」クラスがある。
APIドキュメントにはご丁寧にサンプルまである。
印刷の手順は、

  • 印刷ジョブを作る
  • 印刷対象のノードを渡す
  • 印刷を終了する

の3ステップ。

ここで言うノードは、JavaFXのシーングラフのことで、印刷対象となるGUIのツリー階層のルートノードのこと。
ルートノードを渡せば、それに登録されているサブノードも一式印刷される。

今回は印刷対象にラベル「Hello JavaFX Print.」を子に持つアンカーペイン「printCanvas」を渡す。
「Print」ボタンをクリックした時のイベントハンドラに対して、APIドキュメントのサンプルを参考にprintCanvasを印刷する処理を記述する。

    @FXML
    public void handlePrintButtonAction(ActionEvent event) {
        PrinterJob job = PrinterJob.createPrinterJob();
        job.printPage(printCanvas);
        job.endJob();
    }

※エラー処理を省いているので、コピペ禁止

PrinterJob.createPrinterJob() は「通常使うプリンター」に対してジョブを投入する。
もし対象のプリンタを指定したければプリンタを指定するPrinterJob.createPrinterJob(Printer printer) があるのでそちらを実行するか、setPrinter(Printer printer) メソッドで指定をする。

この状態で実行して「Print」ボタンをクリックすると、CubePDFが起動してPDFの保存先設定が表示され、「変換」をクリックするとPDFが保存される。
f:id:nave_kazu:20170106185703p:plain

保存したPDFを表示すると下記の通り、printCanvas の内容が印刷されていることがわかる。
f:id:nave_kazu:20170106185708p:plain

(仮に)部数の設定

もうちょっといじってみよう。
PrinterJob クラスから取得出来る JobSettings クラスを使用すると、印刷ジョブの設定が出来る。
JobSettings を使って、ジョブの名前やページ範囲や色(モノクロ・カラーの選択)が変更できる。
部数を入力する欄をツールバーに設けているので、その内容を渡して部数設定をしてみる。

    @FXML
    public void handlePrintButtonAction(ActionEvent event) {
        PrinterJob job = PrinterJob.createPrinterJob();

        JobSettings jobSettings = job.getJobSettings();
        jobSettings.setCopies(Integer.parseInt(copiesField.getText()));

        job.printPage(printCanvas);
        job.endJob();
    }

※エラー処理を省いているので、コピペ禁止

部数を入力するエリアに「2」と入れて実行・・・したが、CubePDFの問題なのか分からないが2部印刷が出来ない。
実際のプリンタに出力してみたら2部出た。
何だろ?
f:id:nave_kazu:20170106185712j:plain

ページ設定ダイアログ

印刷時のページ設定を JobSettings などを通して設定も出来るが、印刷時にユーザに設定させることも出来る。
それが「ページ設定ダイアログ」で、下記のように PrinterJob に対して showPageSetupDialog() メソッドで表示させる。

    @FXML
    public void handlePrintButtonAction(ActionEvent event) {
        PrinterJob job = PrinterJob.createPrinterJob();

        JobSettings jobSettings = job.getJobSettings();
        jobSettings.setCopies(Integer.parseInt(copiesField.getText()));

        job.showPageSetupDialog(null);

        job.printPage(printCanvas);
        job.endJob();
    }

※エラー処理を省いているので、コピペ禁止

親のWindowを渡してモーダルにすることも出来るが、面倒だったのでnullを渡してモーダレスで試す。
実行すると下記のようにページ設定ダイアログが表示される。
f:id:nave_kazu:20170106185719p:plain

印刷ダイアログ

印刷対象のプリンタを選択する印刷ダイアログを選択させることも出来る。
それが「印刷ダイアログ」で、下記のように PrinterJob に対して showPrintDialog() メソッドで表示させる。

    @FXML
    public void handlePrintButtonAction(ActionEvent event) {
        PrinterJob job = PrinterJob.createPrinterJob();

        JobSettings jobSettings = job.getJobSettings();
        jobSettings.setCopies(Integer.parseInt(copiesField.getText()));

        job.showPageSetupDialog(null);
        job.showPrintDialog(null);

        job.printPage(printCanvas);
        job.endJob();
    }

※エラー処理を省いているので、コピペ禁止

実行すると下記のように印刷ダイアログが表示される。
f:id:nave_kazu:20170106185723p:plain

まとめ

ページ送りはどうするとかまだ課題はあるが、まずは入り口としてはこのような感じで。