Javaで帳票印刷 PrinterJobとPrintable

最小限の印刷プログラム(SomethingPrintable00.java)

スタートは最小限の印刷プログラムです。

SomethingPrintable00.java

package print01;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.print.PrinterJob;
import java.awt.print.Printable;
import java.awt.print.PageFormat;
import java.awt.print.PrinterException;

public class SomethingPrintable00 implements Printable {
    @Override
    public int print(Graphics g, PageFormat pf, int pageIndex) {
        if (pageIndex != 0) return NO_SUCH_PAGE;
        Graphics2D g2 = (Graphics2D)g;

        g2.drawString("文字を書きます", 72, 120); //文字の印字
        g2.drawLine(72,140,288,160);           //線の描画

        return PAGE_EXISTS;
    }

    public static void main(String[] args) {
        PrinterJob pj = PrinterJob.getPrinterJob();
        pj.setPrintable(new SomethingPrintable00());
        if (pj.printDialog()) {
            try { pj.print(); }
            catch (PrinterException e) {
                System.out.println(e);
            }
        }
    }
}

ちょっとお約束

packageはここではファイルの整理のためです。できるだけ独自ルールを持ち込みたくないのですが、たくさんのプログラムを書いていくと整理がつかなくなりますので最低限の予防措置を取っておきます。

package print01 と書いてあるプログラムのソースファイルは print01 というフォルダに保存し、print01 の親フォルダからコンパイルし、実行します。つまり、

$ javac print01/SomethingPrintable00.java

でコンパイル

$ java print01.SomethingPrintable00

で実行します。

コンパイル後のファイルの位置関係はこんな感じ。

$ tree ./
./
└─ print01
     ├─ SomethingPrintable00.class
     └─ SomethingPrintable00.java

もちろんprint01を変えて好みのフォルダに入れても構いませんし、packageの行を削除すれば、よくあるサンプルプログラムになりますが、フォルダで整理することを強くお勧めします。

実行結果

印刷結果をスキャンした画像です。クリックすると大きくなります。プログラム中の「文字の印字」「線の描画」と注をしてある2件の印字が行われているのが確認できます。

うぐいす色(薄い緑)で書かれた部分は手書きで書き加えたところです。数字は印字位置の座標で、単位はポイント。つまり1/72インチです。

紙の左上が原点のポイント単位の座標系に書き出される

Printableなクラス

Printableインターフェースを持つクラスを作ります。

public class SomethingPrintable00 implements Printable {
    @Override
    public int print(Graphics g, PageFormat pf, int pageIndex) {
        if (pageIndex != 0) return NO_SUCH_PAGE;
        Graphics2D g2 = (Graphics2D)g;

        g2.drawString("文字を書きます", 72, 120); //文字の印字
        g2.drawLine(72,140,288,160);           //線の描画

        return PAGE_EXISTS;
    }

Printableはprintというメソッドを持たねばなりません。引数も決まっています。

このメソッドはシステムによって呼び出されます。自分で呼び出すことはありません。

pageIndexにページ番号が入って呼び出されます。ページ番号は0から始まります。今回は1ページしか書きませんから、pageIndex==0のときは字を書いたり線を引いたりしたあとデータの準備ができたことを知らせ、0でないときはページがないことを知らせます。知らせるのに戻り値をつかいます。return NO_SUCH_PAGE; がページがないことを、return PAGE_EXISTS; がデータの準備ができたことを知らせます。NO_SUCH_PAGE, PAGE_EXISTS は、Printableのフィールドにある int の定数です。実際の値は NO_SUCH_PAGE=1, PAGE_EXISTS=0 ですが気にすることはありません。

ちなみに、

if (pageIndex != 2) return NO_SUCH_PAGE;

とすると、印刷はされません。0ページの時点で NO_SUCH_PAGE と言われると、終わりにするということです(白紙が1枚出力されました)。

printメソッドが呼び出される時にGraphicsのインスタンスが渡されますので、これに対して文字を書いたり、線をひいたりすれば良いのですが、歴史的な事情で、Graphics2D g2 = (Graphics2D)g; を最初に書きます。

Graphics2Dにキャストしておくことで Graphicsクラスのメソッドと、拡張されたGraphics2Dのメソッドの両方を使うことができます。

Printableなクラスに出す印刷指示

mainに書かれたものが、Printableなクラスから印刷物を得る方法です。

mainの中にある必要はありません。帳票印刷などではデータを読み込んだり、計算したりのプログラムの中から、呼び出すことになろうかと思いますが、いまは複雑な準備がいらないのでmainの中に入れてしまったということです。

PrinterJob pj = PrinterJob.getPrinterJob();
pj.setPrintable(new SomethingPrintable00());
if (pj.printDialog()) {
    try { pj.print(); }
    catch (PrinterException e) {
        System.out.println(e);
    }
}

手順としては4ステップ

1. PrinterJobのインスタンスを作って名前をつける(ここではpj)
昔の自分を思い出すとこれがなかなか腑に落ちなかった。何もないところから突然作れるなら、作らなくても使えるようにしておけばいいのに...と
形式的にはPrinterJobクラス内のスタティックメソッドでPrinterJobクラスのインスタンスを作るということでPrinterJobクラスが見慣れてくると、理解できないことはない
2. Printableなクラスのインスタンスを渡す
new SomethingPrintable() で、Printableなクラスのインスタンスを作っている。これを引数に PrinterJobのインスタンス(pj)にsetPrintableメソッドで渡している。
3. 印刷ダイアログを出す
pj.printDialog()でダイアログが表示される。ユーザーがキャンセルしなければ true が返る。falseが返った場合は印刷しないようにできる。
省略できる。その場合はシステムのデフォルトプリンタのデフォルト設定で印刷される
4. 印刷の指示を出す
pj.print() だが、もちろんprint()はPrintableなクラスのメソッドではなく、PrinterJobのメソッド。
例外を捉えるためにtry-catchに入れなければならない。

PrintJobとPrinterJob

印刷について調べると、java.awt.PrintJob を使う方法が解説されていることがあります。PrinterJobと名前が似ているので最初は混乱しました。ここで扱うのは java.awt.print.PrinterJob の方です。

印刷の位置指定は1/72インチ単位(SomethingPrintable.java)

JavaのAPI仕様によると、Graphics2Dでは1/72インチ(=1ポイント)単位で位置を指定すると読めます。72dpiです。ページプリンタの解像度は600dpiと聞きますので一見不足に思えますが、floatやdoubleで小数を使えますから、0.1ポイントで指定出来るだけで720dpiということになります。十分です。

次のプログラムで座標が用紙の端を基準にポイント単位(ptと表記)で指定できることを確認します。

SomethingPrintable.java

package print01;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Font;
import java.awt.print.PrinterJob;
import java.awt.print.Printable;
import java.awt.print.PageFormat;
import java.awt.print.PrinterException;

public class SomethingPrintable implements Printable {
    @Override
    public int print(Graphics g, PageFormat pf, int pageIndex) {
        if (pageIndex != 0) return NO_SUCH_PAGE;
        Graphics2D g2 = (Graphics2D)g;

        g2.setFont(new Font("Serif", Font.PLAIN, 36));
        g2.drawString("36ポイント文字....", 72, 120);

        g2.setFont(new Font("Serif", Font.PLAIN, 24));
        g2.drawString("24ポイント文字.Serif..ijwxyz", 72, 160);

        g2.setFont(new Font("SansSerif", Font.PLAIN, 24));
        g2.drawString("24ポイント文字.SansSerif..ijwxyz", 72, 200);

        g2.setFont(new Font("Monospaced", Font.PLAIN, 24));
        g2.drawString("24ポイント文字.Monospaced..ijwxyz", 72, 240);

        g2.setFont(new Font("Serif", Font.PLAIN, 12));
        g2.drawString("12ポイント文字.Serif..ijwxyz", 144, 280);

        g2.drawString("drawLine(144,300,288,320)",144,300);
        g2.drawLine(144,300,288,320);
        g2.drawString("drawLine(100,400,400,500)",200,450);
        g2.drawLine(100,400,400,500);

        return PAGE_EXISTS;
        //論理フォント: Serif, SansSerif, Monospaced, Dialog, DialogInput
    }

    public static void main(String[] args) {
        PrinterJob pj = PrinterJob.getPrinterJob();
        pj.setPrintable(new SomethingPrintable());
        if (pj.printDialog()) {
            try { pj.print(); }
            catch (PrinterException e) {
                System.out.println(e);
            }
        }
    }
}

単位の換算

1インチ = 72pt = 25.4mm ですから、

ptの数値を72で割り、25.4をかけるとmmに換算できます。

mmの数値を25.4で割り、72をかけるとptに換算できます。

72を25.4にするには72で割って25.4を掛ける

このポイントは文字の大きさを表すポイントと同じものです。

実行結果 その2

印刷したものを実測すると次のようになります。

うぐいす色(薄い緑)で書かれた部分はpt。プログラム中の数値です。

水色(薄い青)で書かれた部分はmm。手書きで書き加えています。

ポイントをmmに換算すると実測値によく合う

紙の上、左の縁からの距離は多少誤差がありますが相対的な位置は正確に72pt単位であるとわかります。

例えば24ポイント文字の行ピッチは40ptですが、

40pt ÷ 72 ✕ 25.4 = 14.111mm 

で実測通りです。

文字の位置はベースライン(英文だとabcdの下の線でpyなどはその下にはみ出す)の左端で指定します。

線はdrawLine(x1,y1,x2,y2)で、点(x1,y1)と点(x2,y2)を線で結びます。

ついでに、Fontで大きさと字体を指定してみました。SerifもSansSerifもプロポーショナルではなくMonospacedと同じ等幅フォントになっているのは意外ですが、システムのフォント設定に依存します。

グラフィクスのメソッド

文字と罫線が描ければよいということで関係するメソッドを拾います

java.awt.Graphics

drawString(String str, int x, int y)	(x,y)に文字列strを書きます
drawLine(int x1, int y1, int x2, int y2)	(x1,y1)と(x2,y2)を線で結びます
drawRect(int x, int y, int width, int height)	(x,y)から幅width,高さheightの四角形を描きます

Graphicsのメソッドは描画位置がintなので、位置指定の最小単位は1/72インチ=0.35mmになります

Lineが2点、Rectが1点と大きさという指定の違いがちょっとわずらわしい。

java.awt.Graphics2D

drawString(String str, int x, int y)
drawString(String str, float x, float y)
draw(Shape s)

Graphics2Dでは文字列はfloatで位置を指定するので、細かな指定ができます。

線は Shape を指定して draw を呼び出すという手順に変わります。もちろんintで良ければdrawLine()も使えます。

draw(new Line2D.Float(float x1, float y1, float x2, float y2))

java.awt.geom.Line2D と Rectangle2D

Shapeはインターフェースでこの下に線、四角、楕円、弧などが用意されていますが、線と四角を拾っておきます。

Line2D.Float(float x1, float y1, float x2, float y2)
Line2D.Double(double x1, double y1, double x2, double y2)
Rectangle2D.Float(float x, float y, float w, float h)
Rectangle2D.Double(double x, double y, double w, double h)

帳票印刷では四角の出番は少なく、線の組み合わせで済ませることが多くなりました。

文字の大きさ、線の太さなどの設定

printメソッドに渡されるGraphics2Dのインスタンス(g2)にデフォルト値があります。変更したければ以下のメソッドで変更後、draw...メソッドを呼び出すとそれを使用して描画します。

setFont(Font font)	フォントの大きさ Graphicsのメソッド
setColor(Color c) 	描画色 Graphicsのメソッド
setPaint(Paint paint)	描画色など Graphics2Dのメソッド
setStroke(Stroke s)	線の太さ Graphics2Dのメソッド

文字の設定

Font(String name, int style, int size)
name は 論理フォントの指定で十分でしょう
"Serif", "SansSerif", "Monospaced", "Dialog", "DialogInput" のどれかですが、
1.6からは定数フィールド値が定義されて
SERIF, SANS_SERIF, MONOSPACED, DIALOG, DIALOG_INPUT と指定できます。
たいして変わりませんがコンパイラがスペルミスを指摘してくれます。
style は PLAIN、BOLD、ITALIC、または BOLD+ITALIC のどれか
size はポイントの値

頻繁に文字の大きさを変更するなら

Font basefont = new Font(Font.SERIF, Font.PLAIN, 12);
Font largefont = new Font(Font.SERIF, Font.PLAIN, 16);
g2.setFont(basefont);
.....

などという使い方が良いかと思います。

Font.をつけるのはどうも...という方は、import static java.awt.Font.*; を使えば

Font basefont = new Font(SERIF, PLAIN, 12);
Font largefont = new Font(SERIF, PLAIN, 16);
g2.setFont(basefont);
.....

と書くことができます。

setPaint()の引数は Paint インタフェース をもつクラスのインスタンスですが、単に色を変えるならColorのインスタンスを使うことで変えられます。import java.awt.Color;を加えることが必要です

g2.setPaint(Color.red);

g2.setColor(Color.red)も使えます。

線の太さ

import java.awt.BasicStroke; が必要です

BasicStroke(float width)

頻繁に変更するなら

BasicStroke boldStroke = new BasicStroke(1.0f);
BasicStroke fineStroke = new BasicStroke(0.0f);
g2.setStroke(fineStroke);
......

などという使い方が良いかと思います。0.0fは一番細い線という事になります。

Lind2Dで線を引く

Line2D.FloatとLine2D.Doubleの両方があります。文字列の位置設定がfloatなので、線もLine2D.Float()を使うことにします。大差はないでしょうが。

Line2D.Float(float x1, float y1, float x2, float y2)

端点の座標をあとから設定することも可能です。

Line2D.Float line = new Line2D.Float()
line.setLine(float x1, float y1, float x2, float y2)

色の変更と線の太さのデモ(LineByShape.java)

LineByShape.java

package print01;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Font;
import static java.awt.Font.*;
import java.awt.Color;
import java.awt.print.PrinterJob;
import java.awt.print.Printable;
import java.awt.print.PageFormat;
import java.awt.print.PrinterException;
import java.awt.BasicStroke;
import java.awt.geom.Line2D;

public class LineByShape implements Printable {
    @Override
    public int print(Graphics g, PageFormat pf, int pageIndex) {
        if (pageIndex != 0) return NO_SUCH_PAGE;
        Graphics2D g2 = (Graphics2D)g;
        g2.setFont(new Font(SERIF, PLAIN, 24));
        g2.drawString("色の変更と線の太さのデモです", 72, 120);
        g2.setFont(new Font(SERIF, PLAIN, 14));
        g2.setColor(Color.red);
        g2.drawString("setColor(Color.red) の後の色です", 72, 140);
        g2.setPaint(Color.blue);
        g2.drawString("setPaint(Color.blue) でも変更可能です", 72, 160);
        g2.drawLine(72,170,288,180);
        g2.drawString("線の色も同時に変わります", 288, 180);
        g2.setPaint(new Color(0x99,0x99,0x99));
        g2.drawString("new Color(0x99,0x99,0x99) で色を作ることも可能です", 72, 200);
        g2.setPaint(Color.black);
        g2.setFont(new Font(SERIF, BOLD, 12));
        g2.drawString("BOLD を試してみます", 72, 220);
        g2.setFont(new Font(SERIF, ITALIC, 12));
        g2.drawString("ITALIC を試してみます", 288, 220);
        Font font = new Font(SERIF, BOLD+ITALIC, 12);
        g2.setFont(font);
        g2.drawString("BOLD+ITALIC は PLAIN=0, BOLD=1, ITALIC=2 なので、3なのでしょう", 72, 240);
        g2.drawString("調べられます。font.getStyle()は "+font.getStyle() + " です。", 72, 260);
        g2.setFont(new Font(SERIF, PLAIN, 12));

        float w ;
        BasicStroke stroke;
        Line2D.Float line = new Line2D.Float();
        w=1.0f;
        stroke = new BasicStroke(w);
        g2.setStroke(stroke);
        line.setLine(100,280,200,380);
        g2.draw(line);
        g2.drawString("_set to:"+w+" /getLineWidth()の値は:"+stroke.getLineWidth(),200,380);
        w=0.5f;
        stroke = new BasicStroke(w);
        g2.setStroke(stroke);
        line.setLine(100,300,200,400);
        g2.draw(line);
        g2.drawString("_set to:"+w+" /getLineWidth()の値は:"+stroke.getLineWidth(),200,400);
        w=0.2f;
        stroke = new BasicStroke(w);
        g2.setStroke(stroke);
        line.setLine(100,320,200,420);
        g2.draw(line);
        g2.drawString("_set to:"+w+" /getLineWidth()の値は:"+stroke.getLineWidth(),200,420);
        w=0.1f;
        stroke = new BasicStroke(w);
        g2.setStroke(stroke);
        line.setLine(100,340,200,440);
        g2.draw(line);
        g2.drawString("_set to:"+w+" /getLineWidth()の値は:"+stroke.getLineWidth(),200,440);
        w=1f/72;
        stroke = new BasicStroke(w);
        g2.setStroke(stroke);
        line.setLine(100,360,200,460);
        g2.draw(line);
        g2.drawString("_set to:"+w+" /getLineWidth()の値は:"+stroke.getLineWidth(),200,460);
        w=0.0f;
        stroke = new BasicStroke(w);
        g2.setStroke(stroke);
        line.setLine(100,380,200,480);
        g2.draw(line);
        g2.drawString("_set to:"+w+" /getLineWidth()の値は:"+stroke.getLineWidth(),200,480);

        return PAGE_EXISTS;
    }

    public static void main(String[] args) {
        PrinterJob pj = PrinterJob.getPrinterJob();
        pj.setPrintable(new LineByShape());
        if (pj.printDialog()) {
            try { pj.print(); }
            catch (PrinterException e) {
                System.out.println(e);
            }
        }
    }
}

おっと。数値リテラルがほとんどintですね。本来は f をつけておくのが好ましいところです。

drawString()はintとfloatの両方のメソッドがあります。intの方が使われていますのでコンパイラは文句を言いません

Line2D.Float#setLine()はfloatとdoubleの両方のメソッドがあります。intはないのですが、int→floatは自動で変換されます。コンパイラは文句を言いません。

floatの w は、1.0のリテラルはdoubleになってしまいますから、1.0fとfloatを指定する必要があります。

見難くなるのでこのプログラムは f を付けないでおきます。

実行結果 その3

色の変更と線の太さのデモ線の太さもポイントで指定する