javaで帳票印刷 印刷位置の微調整

目次

印刷位置の微調整とは

フォームの枠に文字を入れるで作った郵便番号プログラムに印字位置の微調整機能を取り付けます。印刷物により、またプリンターの特性から多少ずれてしまうという時に、プログラム側で手動でずらして合わせようというものです。

ここで使う予定なのは、JSpinnerです。

-0.2

郵便番号枠内印字のプレビュー付き

その前に、印刷プレビューに従ってプレビューの機能を付け、g2.drawString(...で直接書いていた印字を、AjustStringクラスを用いた印字へ書き換えます。

先に印字結果を示します。


旧タイプの印字は黒字で、AjustStringクラスを用いた印字は青で両方重ねています。黒、青片方ずつの図も作りました。このページのボタン操作で片方ずつの印字結果を見ることができます。

旧タイプは、ずれがあるように見えます。四角の枠の左側に文字を配置した後、一律に2.0mmだけ右に寄せていました。ところが、枠の幅が左の3つは6.4mm、右の4つは5.8mmなので、小さい枠では枠の右側に寄ってしまうのです。

AjustStringでは一文字ずつにして枠の幅を指定し、センタリングで書いているので、全部の文字が枠の中心に収まっています。

赤枠も書くようにしました。ピッタリと入る様子をスクリーンショットだけで示したかったことが大きいですが、郵便番号の枠のない用紙に印刷する場合も考慮に入れています。もっとも郵便局の読み取りでは赤枠はなくても良い(機械は赤をドロップして黒だけ読む)ので、文字を手書きしないならいらないのですが。

プログラム(ZipBidou.java)

AjustStringの使い勝手の良さを実感していただくため、黒字も残してあります。

ZipBidou.java

package print01;
import java.awt.*;
import java.awt.print.*;
import java.awt.geom.Line2D;
import javax.print.attribute.Attribute;
import javax.print.attribute.HashPrintJobAttributeSet;
import javax.print.attribute.HashPrintRequestAttributeSet;
import javax.print.attribute.PrintJobAttributeSet;
import javax.print.attribute.PrintRequestAttributeSet;
import javax.print.attribute.standard.MediaSize;
import javax.print.attribute.standard.MediaSizeName;
import javax.print.attribute.standard.MediaPrintableArea;
import javax.print.attribute.standard.MediaTray;
import javax.print.attribute.standard.OrientationRequested;
import javax.swing.*;  
import static java.awt.Font.*;
import static java.awt.RenderingHints.*;

public class ZipBidou extends JPanel implements Printable {
    double pcardlongside  = 148d*72/25.4; // 
    double pcardshortside = 100d*72/25.4; // w=283.46457 h=419.52756

    double pwidth = pcardshortside;  //用紙の横幅。用紙を変更する時はここ
    double pheight = pcardlongside;  //用紙の縦幅
    double wZh = pwidth/pheight;  //横÷縦
    Dimension prevsize;   //previewSize preferredSizeで使用
    double scale;         //Graphics2D#.scale()に使用
    public ZipBidou(){
        setBackground(Color.white);
        prevsize = new Dimension(); //(int)pwidth,(int)pheight);
        prevsize.setSize(pwidth*2,pheight); //newではintしか認められないがsetはdoubleも許容される
        setPreferredSize(prevsize);       //これは初期値 (new Dimension(595,841)におなじ)
    }

    @Override
    public void paintComponent(Graphics g){
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D) g;
        int viewpw = getParent().getWidth();
        int viewph = getParent().getHeight();
        JScrollPane sp = (JScrollPane) getParent().getParent();
        int hbarh = sp.getHorizontalScrollBar().getHeight();
        int vbarw = sp.getVerticalScrollBar().getWidth();
        boolean hbarAri = sp.getHorizontalScrollBar().isVisible();
        boolean vbarAri = sp.getVerticalScrollBar().isVisible();
        if (vbarAri) viewpw += vbarw;
        if (hbarAri) viewph += hbarh;
        if((double)(viewpw-vbarw)/viewph > wZh){
            prevsize.setSize(viewpw-vbarw, (viewpw-vbarw)/wZh);
            scale = (viewpw-vbarw)/pwidth;
        }else if( wZh > (double)viewpw / (viewph-hbarh)  ){
            prevsize.setSize((viewph-hbarh)*wZh, viewph-hbarh);
            scale = (viewph-hbarh)/pheight;
        }else{
            if ( (double)viewpw/viewph > wZh ) {
                prevsize.setSize(viewph*wZh, viewph);
                scale = viewph/pheight;
            }else{
                prevsize.setSize(viewpw, viewpw/wZh);
                scale = viewpw/pwidth;
            }
        }
        setPreferredSize(prevsize);
        //System.out.println(scale); //test
        g2.scale(scale,scale);
        drawPage(g2);
    }
    @Override
    public int print(Graphics g, PageFormat pf, int pageIndex) {
        if (pageIndex != 0) return NO_SUCH_PAGE;
        Graphics2D g2 = (Graphics2D) g;
        drawPage(g2);
        return PAGE_EXISTS;
    }
    public void drawPage(Graphics2D g2){
        g2.setRenderingHint(KEY_ANTIALIASING,VALUE_ANTIALIAS_ON);
        g2.setRenderingHint(KEY_TEXT_ANTIALIASING,VALUE_TEXT_ANTIALIAS_ON);
        float hbas1 = 44.0f;
        float hbas2 = 66.0f;
        float hptch1 = 13.9f/2;
        float hptch2 = 20.5f/3;
        float hwbox1 = 6.4f;
        float hwbox2 = 5.8f;
        float vbas  = 20.3f;
        float vwrec = 8.3f;
        float mm2pt  = 72/25.4f;
        String zip = "2345678";
        g2.setFont(new Font(SANS_SERIF, PLAIN, 16));
        //g2.setFont(new Font("SansSerif", Font.PLAIN, 16));
        AjustString kp;
        BasicStroke finestroke = new BasicStroke(0.4f);
        Linemm line = new Linemm(); //drawline with new class
        g2.setPaint(Color.black);
        float vbk = 1.5f; //vbasそのままだと下線に接するのでもどす値(mm)
        float hfw = 2.0f; //vbasそのままだと左線に接するので進める値(mm)
        //今までの文字
        for(int i=0; 3>i; i++){
            g2.drawString(zip.substring(i,i+1),(hbas1+hfw+hptch1*i)*mm2pt,(vbas-vbk)*mm2pt);
        }
        for(int i=0; 4>i; i++){
            g2.drawString(zip.substring(i+3,i+4),(hbas2+hfw+hptch2*i)*mm2pt,(vbas-vbk)*mm2pt);
        }
        //AjustStringを使った文字(blue)
        g2.setPaint(Color.blue);
        for(int i=0; 3>i; i++){
            kp = new AjustString(g2,zip.substring(i,i+1),hwbox1);
            kp.drawCenter(hbas1+hptch1*i,vbas-vbk);
        }
        for(int i=0; 4>i; i++){
            kp = new AjustString(g2,zip.substring(i+3,i+4),hwbox2);
            kp.drawCenter(hbas2+hptch2*i,vbas-vbk);
        }
        //枠(red)
        g2.setPaint(Color.red);
        g2.setStroke(finestroke);
        for(int i=0; 3>i; i++){
            line.setLR(hbas1+hptch1*i,hbas1+hwbox1+hptch1*i);
            line.setY(vbas-vwrec);
            g2.draw(line); //Rectangle TOP
            line.setY(vbas);
            g2.draw(line); //Rectangle bottom
            line.setTB(vbas-vwrec,vbas);
            line.setX(hbas1+hptch1*i);
            g2.draw(line); //Rectangle left
            line.setX(hbas1+hwbox1+hptch1*i);
            g2.draw(line); //Rectangle right
        }
        for(int i=0; 4>i; i++){
            line.setLR(hbas2+hptch2*i,hbas2+hwbox2+hptch2*i);
            line.setY(vbas-vwrec);
            g2.draw(line); //Rectangle TOP
            line.setY(vbas);
            g2.draw(line); //Rectangle bottom
            line.setTB(vbas-vwrec,vbas);
            line.setX(hbas2+hptch2*i);
            g2.draw(line); //Rectangle left
            line.setX(hbas2+hwbox2+hptch2*i);
            g2.draw(line); //Rectangle right
        }

        g2.setPaint(Color.black);
    }

    public static void main(String[] args) {
        PrinterJob pj = PrinterJob.getPrinterJob();
        PrintRequestAttributeSet reqset = new HashPrintRequestAttributeSet();
        //MediaSizeName medname = MediaSizeName.ISO_A4;
        MediaSizeName medname = MediaSizeName.JAPANESE_POSTCARD;
        reqset.add(medname);

        MediaSize medsize = MediaSize.getMediaSizeForName(medname);
        float medwidth  = medsize.getX(MediaPrintableArea.MM);
        float medheight = medsize.getY(MediaPrintableArea.MM);
        //System.out.println("w="+medwidth + " h="+medheight);//test
        //System.out.println("w="+medwidth*72/25.4f + " h="+medheight*72/25.4f);//test

        float topmm   = 5.5f;  //landscape前のTOP
        float bottomm = 7.7f;
        float leftmm  = 6.6f;
        float rightmm = 5.5f;
        reqset.add(new MediaPrintableArea(
           leftmm, topmm,
           (medwidth - leftmm - rightmm),
           (medheight - topmm - bottomm), MediaPrintableArea.MM));
        //reqset.add(OrientationRequested.REVERSE_LANDSCAPE);
        //reqset.add(OrientationRequested.LANDSCAPE);
        reqset.add(OrientationRequested.PORTRAIT);

        ZipBidou zipbidou = new ZipBidou();
        JScrollPane scrollPane = new JScrollPane(zipbidou); 
        String[] goOrNot = {"印刷","中止"};
        JOptionPane pane = new JOptionPane(
                                   scrollPane, 
                                   JOptionPane.PLAIN_MESSAGE, 
                                   JOptionPane.DEFAULT_OPTION,
                                   null,goOrNot,goOrNot[0]
                               );
        //JDialog dialog = pane.createDialog( null, "印刷プレビュー" );
        JDialog dialog = pane.createDialog("印刷プレビュー" );
        dialog.pack();
        dialog.setResizable(true);
        dialog.setVisible(true);
        String svalue = (String)pane.getValue();
        if (svalue==null || !svalue.equals("印刷")) System.exit(0);



        pj.setPrintable(zipbidou);  //printable instance
        if (pj.printDialog( reqset )) {
            boolean debug = true;
            if(debug){
                Attribute[] attrs = reqset.toArray(); //test
                for(int n=0 ; attrs.length > n ; n++){ //test
                    System.out.println( attrs[n].getName()+":"+ attrs[n].toString()+";; ");
                }
            }
            try { pj.print( reqset ); }
            catch (PrinterException e) {
                System.err.println(e);
            }
        }
    }
}

平行移動・拡大・回転

java.awt.Graphics2D のメソッドである translate(double tx, double ty)を使います。

g2.translate(2,0);と設定しておけば、描画にx,yと指定したものが、x+2,y に指定したことになります。

すでに、印刷プレビューで scale(double sx, double sy) を紹介しています。こちらは、g2.scale(sx,sy) と設定しておけば、描画にx,yで指定したものが、sx*x,sy*y に指定したことになります。

これらは AffineTransform と呼ばれるもので、ユーザー空間の座標をデバイスに依存する座標に変換する方法を定義するものと説明されています。

一般的なx,y,zの座標変換のうちx,yの部分だけを取り出したものになります。

API仕様によると、

      [ x']   [  m00  m01  m02  ] [ x ]   [ m00x + m01y + m02 ]
      [ y'] = [  m10  m11  m12  ] [ y ] = [ m10x + m11y + m12 ]
      [ 1 ]   [   0    0    1   ] [ 1 ]   [         1         ]

と一般化されています。

translateは

public abstract void translate(double tx, double ty)
          [   1    0    tx  ]
          [   0    1    ty  ]
          [   0    0    1   ]

scaleは

public abstract void scale(double sx, double sy)
          [   sx   0    0   ]
          [   0    sy   0   ]
          [   0    0    1   ]

回転もあります。thetaはラジアンです。

public abstract void rotate(double theta)
          [   cos(theta)    -sin(theta)    0   ]
          [   sin(theta)     cos(theta)    0   ]
          [       0              0         1   ]

Spinnerの取り付けまで

1. new SpinnerNumberModel で数値を扱うSpinnerModelを作成します。

2. それを引数にSpinnerのインスタンスを作ります。hspinner,spinner

3. コラム幅を5文字分にします。

4. 上、右のラベルと一緒にSpinner用のパネルに貼ります。

5. 2つのSpinnerインスタンスを引数にZipBidouのインスタンスを作ります。

6. それをscrollPaneに入れます。

7. さらにpreviewSpinnerPanelを作って、scrollPaneとSpinner用のパネルを入れ

8. JOptionPaneに設置してダイアログを作ります。

ZipBidou.javaのmain

SpinnerModel vmodel = new SpinnerNumberModel(0.0d, -10.0d, +10.0d, 0.10d);
SpinnerModel hmodel = new SpinnerNumberModel(0.0d, -10.0d, +10.0d, 0.10d);
JSpinner vspinner = new JSpinner(vmodel);
JSpinner hspinner = new JSpinner(hmodel);
((JSpinner.DefaultEditor)(vspinner.getEditor())).getTextField().setColumns(5);
((JSpinner.DefaultEditor)(hspinner.getEditor())).getTextField().setColumns(5);
//印刷位置の微調整(±10mmまで) 
JLabel vlabel = new JLabel("上へ(mm)");
JLabel hlabel = new JLabel("  右へ(mm)");
JPanel spnnpanel = new JPanel();
spnnpanel.add(vlabel);
spnnpanel.add(vspinner);
spnnpanel.add(hlabel);
spnnpanel.add(hspinner);
ZipBidou zipbidou = new ZipBidou(hspinner,vspinner);
JScrollPane scrollPane = new JScrollPane(zipbidou);

JPanel previewSpinnerPanel = new JPanel(new BorderLayout());
previewSpinnerPanel.add(scrollPane,BorderLayout.CENTER);
previewSpinnerPanel.add(spnnpanel,BorderLayout.SOUTH);
String[] goOrNot = {"印刷","中止"};
JOptionPane pane = new JOptionPane(
              previewSpinnerPanel, //p
              JOptionPane.PLAIN_MESSAGE, 
              JOptionPane.DEFAULT_OPTION,
              null,goOrNot,goOrNot[0]
               );

コラム幅を5文字分にするというところがトリッキーです。これをしないとマイナスが出た時に小数以下が隠れたりします。

vspinner.getEditor()でJComponentが得られます。

しかし、実はこれはJComponentを継承したJSpinner.NumberEditorなのです。

JSpinner.NumberEditorの親のJSpinner.DefaultEditorにキャストします。

JSpinner.DefaultEditorのTextFieldをDefaultEditor#getTextField()で得ます。

JSpinner.NumberEditorにキャストしても良かったのですが、getTextField()メソッドがDefaultEditorのメソッドだからです。

得られたTextField()を.setColumns(5)で5カラムの幅にします。

ここまでで、

import javax.swing.JSpinner;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;

が必要ですが、import javax.swing.*; をしてしまったので、なくても構いません。

javax.swing.JSpinner.NumberEditorまでの継承関係です。

java.lang.Object
   java.awt.Component
       java.awt.Container
           javax.swing.JComponent
               javax.swing.JPanel
                   javax.swing.JSpinner.DefaultEditor
                       javax.swing.JSpinner.NumberEditor 

ZipBidou本体にリスナーを加える

Spinnerへの操作をZipBidouのインスタンスが感知できるようにします。

操作によってプレビューが変化しますが、これは必ずしも必要ではありません。

Spinner変更の感知はChangeListenerで行います。implementsして、addChangeListener(this)します。

ZipBidou.javaのフィールドとコンストラクタ

import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;

public class ZipBidou extends JPanel implements Printable,ChangeListener {
    double pcardlongside  = 148d*72/25.4; // 
    double pcardshortside = 100d*72/25.4; // w=283.46457 h=419.52756
    ....
    JSpinner vspinner,hspinner;

    public ZipBidou(JSpinner hspinner, JSpinner vspinner){
        this.hspinner = hspinner;
        this.vspinner = vspinner;
        hspinner.addChangeListener(this);
        vspinner.addChangeListener(this);
        setBackground(Color.white);
        prevsize = new Dimension(); //(int)pwidth,(int)pheight);
        prevsize.setSize(pwidth,pheight); //newではintしか認められないがsetはdoubleも許容される
        setPreferredSize(prevsize);       //これは初期値 (new Dimension(595,841)におなじ)
        //this.getParent().setPreferredSize(prevsize);
    }

implementsしたら、stateChanged(ChangeEvent e)メソッドを用意する必要があります。

    @Override
    public void stateChanged(ChangeEvent e){
        repaint();
    }

だけです。

プレビュー、印刷に反映させる

JSpinner#getValue()の値はObjectとなっているが、SpinnerNumberModelの場合はdouble

ここでtranslateの値として使用する。y方向については▲で上に移動させるために「上へ」として、正負を逆にしている。

ZipBidou.javaのdrawPage()冒頭

    public void drawPage(Graphics2D g2){
        g2.setRenderingHint(KEY_ANTIALIASING,VALUE_ANTIALIAS_ON);
        g2.setRenderingHint(KEY_TEXT_ANTIALIASING,VALUE_TEXT_ANTIALIAS_ON);
        double trdx = (double)hspinner.getValue();
        double trdy = (double)vspinner.getValue();
        float mm2pt  = 72/25.4f;
        g2.translate(trdx*mm2pt,-trdy*mm2pt);
        float hbas1 = 44.0f;
        float hbas2 = 66.0f;

プレビューの様子

プログラム(ZipBidou.java)

実用には枠はない方がよい場合が多いでしょう。プレビューの見栄えのために残してあります。

ZipBidou.java

package print01;
import java.awt.*;
import java.awt.print.*;
import java.awt.geom.Line2D;
import javax.print.attribute.Attribute;
import javax.print.attribute.HashPrintJobAttributeSet;
import javax.print.attribute.HashPrintRequestAttributeSet;
import javax.print.attribute.PrintJobAttributeSet;
import javax.print.attribute.PrintRequestAttributeSet;
import javax.print.attribute.standard.MediaSize;
import javax.print.attribute.standard.MediaSizeName;
import javax.print.attribute.standard.MediaPrintableArea;
import javax.print.attribute.standard.MediaTray;
import javax.print.attribute.standard.OrientationRequested;
import javax.swing.*;  
import static java.awt.Font.*;
import static java.awt.RenderingHints.*;
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;

public class ZipBidou extends JPanel implements Printable,ChangeListener {
    double pcardlongside  = 148d*72/25.4; // 
    double pcardshortside = 100d*72/25.4; // w=283.46457 h=419.52756

    double pwidth = pcardshortside;  //用紙の横幅。用紙を変更する時はここ
    double pheight = pcardlongside;  //用紙の縦幅
    double wZh = pwidth/pheight;  //横÷縦
    Dimension prevsize;   //previewSize preferredSizeで使用
    double scale;         //Graphics2D#.scale()に使用
    JSpinner vspinner,hspinner;
    public ZipBidou(JSpinner hspinner, JSpinner vspinner){
        this.hspinner = hspinner;
        this.vspinner = vspinner;
        hspinner.addChangeListener(this);
        vspinner.addChangeListener(this);
        setBackground(Color.white);
        prevsize = new Dimension(); //(int)pwidth,(int)pheight);
        prevsize.setSize(pwidth,pheight); //newではintしか認められないがsetはdoubleも許容される
        setPreferredSize(prevsize);       //これは初期値
        //this.getParent().setPreferredSize(prevsize);
    }
    @Override
    public void stateChanged(ChangeEvent e){
        repaint();
    }
    @Override
    public void paintComponent(Graphics g){
        super.paintComponent(g);
        
        Graphics2D g2 = (Graphics2D) g;
        int viewpw = getParent().getWidth();
        int viewph = getParent().getHeight();
        JScrollPane sp = (JScrollPane) getParent().getParent();
        int hbarh = sp.getHorizontalScrollBar().getHeight();
        int vbarw = sp.getVerticalScrollBar().getWidth();
        boolean hbarAri = sp.getHorizontalScrollBar().isVisible();
        boolean vbarAri = sp.getVerticalScrollBar().isVisible();
        if (vbarAri) viewpw += vbarw;
        if (hbarAri) viewph += hbarh;
        if((double)(viewpw-vbarw)/viewph > wZh){
            prevsize.setSize(viewpw-vbarw, (viewpw-vbarw)/wZh);
            scale = (viewpw-vbarw)/pwidth;
        }else if( wZh > (double)viewpw / (viewph-hbarh)  ){
            prevsize.setSize((viewph-hbarh)*wZh, viewph-hbarh);
            scale = (viewph-hbarh)/pheight;
        }else{
            if ( (double)viewpw/viewph > wZh ) {
                prevsize.setSize(viewph*wZh, viewph);
                scale = viewph/pheight;
            }else{
                prevsize.setSize(viewpw, viewpw/wZh);
                scale = viewpw/pwidth;
            }
        }
        setPreferredSize(prevsize);
        //System.out.println(scale); //test
        g2.scale(scale,scale);
        drawPage(g2);
    }
    @Override
    public int print(Graphics g, PageFormat pf, int pageIndex) {
        if (pageIndex != 0) return NO_SUCH_PAGE;
        Graphics2D g2 = (Graphics2D) g;
        drawPage(g2);
        return PAGE_EXISTS;
    }
    public void drawPage(Graphics2D g2){
        g2.setRenderingHint(KEY_ANTIALIASING,VALUE_ANTIALIAS_ON);
        g2.setRenderingHint(KEY_TEXT_ANTIALIASING,VALUE_TEXT_ANTIALIAS_ON);
        double trdx = (double)hspinner.getValue();
        double trdy = (double)vspinner.getValue();
        float mm2pt  = 72/25.4f;
        g2.translate(trdx*mm2pt,-trdy*mm2pt);
        float hbas1 = 44.0f;
        float hbas2 = 66.0f;
        float hptch1 = 13.9f/2;
        float hptch2 = 20.5f/3;
        float hwbox1 = 6.4f;
        float hwbox2 = 5.8f;
        float vbas  = 20.3f;
        float vwrec = 8.3f;
        String zip = "2345678";
        g2.setFont(new Font(SANS_SERIF, PLAIN, 16));
        AjustString kp;
        BasicStroke finestroke = new BasicStroke(0.4f);
        Linemm line = new Linemm(); //drawline with new class
        g2.setPaint(Color.black);
        float vbk = 1.5f; //vbasそのままだと下線に接するのでもどす値(mm)
        //g2.setPaint(Color.blue);
        for(int i=0; 3>i; i++){
            kp = new AjustString(g2,zip.substring(i,i+1),hwbox1);
            kp.drawCenter(hbas1+hptch1*i,vbas-vbk);
        }
        for(int i=0; 4>i; i++){
            kp = new AjustString(g2,zip.substring(i+3,i+4),hwbox2);
            kp.drawCenter(hbas2+hptch2*i,vbas-vbk);
        }
        //枠(red)
        g2.setPaint(Color.red);
        g2.setStroke(finestroke);
        for(int i=0; 3>i; i++){
            line.setLR(hbas1+hptch1*i,hbas1+hwbox1+hptch1*i);
            line.setY(vbas-vwrec);
            g2.draw(line); //Rectangle TOP
            line.setY(vbas);
            g2.draw(line); //Rectangle bottom
            line.setTB(vbas-vwrec,vbas);
            line.setX(hbas1+hptch1*i);
            g2.draw(line); //Rectangle left
            line.setX(hbas1+hwbox1+hptch1*i);
            g2.draw(line); //Rectangle right
        }
        for(int i=0; 4>i; i++){
            line.setLR(hbas2+hptch2*i,hbas2+hwbox2+hptch2*i);
            line.setY(vbas-vwrec);
            g2.draw(line); //Rectangle TOP
            line.setY(vbas);
            g2.draw(line); //Rectangle bottom
            line.setTB(vbas-vwrec,vbas);
            line.setX(hbas2+hptch2*i);
            g2.draw(line); //Rectangle left
            line.setX(hbas2+hwbox2+hptch2*i);
            g2.draw(line); //Rectangle right
        }
        g2.setPaint(Color.black);
        //枠はここまで
    }

    public static void main(String[] args) {

        PrinterJob pj = PrinterJob.getPrinterJob();
        PrintRequestAttributeSet reqset = new HashPrintRequestAttributeSet();
        //MediaSizeName medname = MediaSizeName.ISO_A4;
        MediaSizeName medname = MediaSizeName.JAPANESE_POSTCARD;
        reqset.add(medname);

        MediaSize medsize = MediaSize.getMediaSizeForName(medname);
        float medwidth  = medsize.getX(MediaPrintableArea.MM);
        float medheight = medsize.getY(MediaPrintableArea.MM);
        float topmm   = 5.5f;  //landscape前のTOP
        float bottomm = 7.7f;
        float leftmm  = 6.6f;
        float rightmm = 5.5f;
        reqset.add(new MediaPrintableArea(
           leftmm, topmm,
           (medwidth - leftmm - rightmm),
           (medheight - topmm - bottomm), MediaPrintableArea.MM));
        //reqset.add(OrientationRequested.REVERSE_LANDSCAPE);
        //reqset.add(OrientationRequested.LANDSCAPE);
        reqset.add(OrientationRequested.PORTRAIT);

        SpinnerModel vmodel = new SpinnerNumberModel(0.0d, -10.0d, +10.0d, 0.10d);
        SpinnerModel hmodel = new SpinnerNumberModel(0.0d, -10.0d, +10.0d, 0.10d);
        JSpinner vspinner = new JSpinner(vmodel);
        JSpinner hspinner = new JSpinner(hmodel);
        ((JSpinner.DefaultEditor)(vspinner.getEditor())).getTextField().setColumns(5);
        ((JSpinner.DefaultEditor)(hspinner.getEditor())).getTextField().setColumns(5);
        JLabel vlabel = new JLabel("上へ(mm)");
        JLabel hlabel = new JLabel("  右へ(mm)");
        JPanel spnnpanel = new JPanel();
        spnnpanel.add(vlabel);
        spnnpanel.add(vspinner);
        spnnpanel.add(hlabel);
        spnnpanel.add(hspinner);
        ZipBidou zipbidou = new ZipBidou(hspinner,vspinner);
        JScrollPane scrollPane = new JScrollPane(zipbidou);

        JPanel previewSpinnerPanel = new JPanel(new BorderLayout()); //p
        previewSpinnerPanel.add(scrollPane,BorderLayout.CENTER); //p
        previewSpinnerPanel.add(spnnpanel,BorderLayout.SOUTH);       //p

        String[] goOrNot = {"印刷","中止"};
        JOptionPane pane = new JOptionPane(
                                   //b notborderbox,
                                   previewSpinnerPanel, //p
                                   JOptionPane.PLAIN_MESSAGE, 
                                   JOptionPane.DEFAULT_OPTION,
                                   null,goOrNot,goOrNot[0]
                               );
        //JDialog dialog = pane.createDialog( null, "印刷プレビュー" );
        JDialog dialog = pane.createDialog("印刷プレビュー" );
        dialog.pack();
        dialog.setResizable(true);
        dialog.setVisible(true);
        String svalue = (String)pane.getValue();
        do{
            if (svalue!=null && svalue.equals("印刷")) {
                pj.setPrintable(zipbidou);  //printable instance
                if (pj.printDialog( reqset )) {
                    boolean debug = true;
                    try { pj.print( reqset ); }
                    catch (PrinterException e) {
                        System.err.println(e);
                    }
                }
                dialog.setVisible(true);
                svalue = (String)pane.getValue();
            }
        }while(svalue!=null && !svalue.equals("中止"));
        System.exit(0);
    }
}

印刷後、Javaが終了しないというミスがありました。do以下を書き換えました。印刷後位置を調整して再印刷できます。ただし、枠も一緒に動きます。実用では枠は描画しないので問題ありません。(2017-08-30)