javaで帳票印刷 罫線を引く

目次

復習

線を引くには、Graphics で、

drawLine(int x1, int y1, int x2, int y2)

を使いました。位置を細かく指定するには、Graphic2D で、Line2D.FloatまたはLine2D.Doubleでインスタンスを作ってから、

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

などとしました。文字の位置がfloatなので線もそれに合わせてfloatを使うこととし、Line2D.Floatのインスタンスは使いまわして、setLine()メソッドで位置を変えていく事もやってみました。setStroke()メソッドを使って線の太さも変えられました。

float mm2pt  = 72/25.4f;
Line2D.Float line = new Line2D.Float();
float w=1.0f;
BasicStroke stroke = new BasicStroke(w);
g2.setStroke(stroke);
line.setLine(10f*mm2pt,28f*mm2pt,20f*mm2pt,38f*mm2pt);
g2.draw(line);

単位はptに換算する必要があり、数が多いと煩わしい。文字についてAjustStringを用意したように、線についてもmmで指定できる道具を作ることにします。

線をmmで扱うクラスを自作する

経験上、帳票では同じ長さの線を平行移動しながら何本も引くことが必要になります。

そこで、setHorSpan( float x1, float x2 )で横の範囲を設定しておいて、setHorAt(float y)で縦位置だけを変えながら線を引くというやりかたでやっていました。縦線はsetVerSpan(),setVerAt()で描いていくのですが、どうも見にくい。今回は、変えてみます。わかりやすくなるかどうか...

Linemmというクラスを作ります。

コンストラクタ

Linemm();
  extends Line2D.Float なクラスです。mm2pt = 72/25.4f;, pt2mm = 25.4f/72; を用意します。
Linemm(float x1mm, float y1mm, float x2mm, float y2mm);
  x1,x2,y1,y2 をptに換算して線のインスタンスを作ります。

メソッド

setTB(float y1mm, flaot y2mm)
  線の2つの端点のy座標をそれぞれ y1mm*mm2pt, y2mm*mm2pt とします。xは変化させません。
  このメソッドは縦線を引くことを想定しています。
setX(float xmm)
  線の2つの端点のx座標を2つとも xmm*mm2pt とします。yは変化させません。
  このメソッドは縦線を引くことを想定しています。
setLR(float x1mm, flaot x2mm)
  線の2つの端点のx座標をそれぞれ x1mm*mm2pt, x2mm*mm2pt とします。yは変化させません。
  このメソッドは横線を引くことを想定しています。
setY(float ymm)
  線の2つの端点のy座標を2つとも ymm*mm2pt とします。xは変化させません。
  このメソッドは横線を引くことを想定しています。
setPP( float x1mm,float y1mm, float x2mm, float y2mm )
  線の2つの端点の座標をそれぞれ ()x1mm*mm2pt,y1mm*mm2pt), (x2mm*mm2pt,y2mm*mm2pt) とします。

当初、setTB()もsetY()として、引数の数で異なるメソッドとしたが、引数が数式になって長くなると、人の目には判断が難しい。メソッド名を変えてコンパイラに引数の数のチェックをしてもらうのが良いと考えた。

LR, TBなど名前を変更する時に間違えていたのを直しました(2017-05-08)

プログラムソース (Linemm.java)

Linemm.java

package print01;

import java.awt.geom.Line2D;

public class Linemm extends Line2D.Float {
    float x1, y1, x2, y2;
    float mm2pt, pt2mm;
    public Linemm(){
        mm2pt = 72/25.4f;
        pt2mm = 25.4f/72;
    }
    public Linemm(float x1mm, float y1mm, float x2mm, float y2mm){
        mm2pt = 72/25.4f;
        pt2mm = 25.4f/72;
        x1 = x1mm * mm2pt;
        y1 = y1mm * mm2pt;
        x2 = x2mm * mm2pt;
        y2 = y2mm * mm2pt;
    }
    public void setX(float xmm) {
        x1 = xmm * mm2pt;
        x2 = x1;
        super.setLine( x1, y1, x2, y2); 
    }
    public void setY(float ymm) {
        y1 = ymm * mm2pt;
        y2 = y1;
        super.setLine( x1, y1, x2, y2); 
    }
    public void setLR( float x1mm, float x2mm ) {
        x1 = x1mm * mm2pt;
        x2 = x2mm * mm2pt;
        super.setLine( x1, y1, x2, y2); 
    }
    public void setTB( float y1mm, float y2mm ) {
        y1 = y1mm * mm2pt;
        y2 = y2mm * mm2pt;
        super.setLine( x1, y1, x2, y2); 
    }
    public void setPP( float x1mm,float y1mm, float x2mm, float y2mm ) {
        x1 = x1mm * mm2pt;
        x2 = x2mm * mm2pt;
        y1 = y1mm * mm2pt;
        y2 = y2mm * mm2pt;
        super.setLine( x1, y1, x2, y2); 
    }
}

Tsuchi.javaに線の部分を追加する

Tsuchi.javaのprint()に追加します。

Tsuchi.javaに追加

        float hwall = hwkm+hwtn+hptch*4;
        BasicStroke boldstroke = new BasicStroke(1.0f);
        BasicStroke medmstroke = new BasicStroke(0.7f);
        BasicStroke thinstroke = new BasicStroke(0.0f);
        g2.setStroke(boldstroke);
        Linemm line = new Linemm();
        line.setTB(vthtop, vtdtop+vptch*kams.length);
        line.setX(hbas);
        g2.draw(line);
        line.setX(hbas+hwall);
        g2.draw(line);
        line.setLR(hbas, hbas+hwall);
        line.setY(vthtop);
        g2.draw(line);
        line.setY(vtdtop+vptch*kams.length);
        g2.draw(line);
        g2.setStroke(medmstroke);
        line.setY(vtdtop);
        g2.draw(line);

        g2.setStroke(thinstroke);
        line.setLR(hbas, hbas+hwall);
        for (int k=1; kams.length>k; k++){ //1 not 0
            line.setY(vtdtop+vptch*k);
            g2.draw(line);
        }
        line.setTB(vthtop, vtdtop+vptch*kams.length);
        line.setX(hbas+hwkm);
        g2.draw(line);
        for (int i=0; 4>i; i++){
            line.setX(hbas+hwkm+hwtn+hptch*i);
            g2.draw(line);
        }

実行結果

線が入ることで均等割付けがそれらしく見える。この帳票印刷の始めは印刷業者から納入された印刷物にデータを書き込むところからのスタートだったので、昔は線を描かなかった。

右半分が空いているのは出欠の記録のため。

通知表の文字部分+線

入りきらない文字列の対策

科目名の"コミュニケーⅠ"が省略形でなく、本来の"コミュニケーション英語Ⅰ"の場合はどうなるか。

プログラムはそのままでデータだけ長い場合

入るだけ印字

プログラムを変更 その1 2行にする

kp = new AjustString(...の後、すぐに書かずに、kp.hasNext() で入りきるかを確認します。

入りきらなければ入る分を左寄せで高めに書き、残りをもう一度 kp = new AjustString(...にかけて、今度は右寄せで低めに書きます。

kp = new AjustString(g2, kams[k] ,hwkm-2f);
if(kp.hasNext()){  //長い科目名を2行に
    kp.drawLeft(hbas+1f,vm+padbtm*3/4-vptch/2);
    kp = new AjustString(g2, kams[k] ,hwkm-2f, kp.getNextPt());
    kp.drawRight(hbas+1f,vm+padbtm/2);
}else{
    kp.drawKintou(hbas+1f,vm);
}

このようになります。

入らない部分を2行目に印字

プログラムを変更 その2 小さな文字にする

入りきらなければフォントを小さくして、もう一度 kp = new AjustString(...にかけます。今回は6ポイントまで下げなければならないので、美しくはありませんが、1,2割小さくすれば良い場合は美しく決まります。また、何回か繰り返して少しずつポイントを下げていくという手もあります。

kp = new AjustString(g2, kams[k] ,hwkm-2f);
if(kp.hasNext()){  //長い科目名をフォントを変えて
    g2.setFont(new Font("Serif", Font.PLAIN, 6));
    kp = new AjustString(g2, kams[k] ,hwkm-2f);
    kp.drawKintou(hbas+1f,vm);
    g2.setFont(font10);
}else{
    kp.drawKintou(hbas+1f,vm);
}

このようになります。

字を小さくして印字

縦書きの均等割付

今までの均等割付けと同様な考え方ですが、文字列はもちろん、文字ごとの印字高さを返すメソッドがありません。フォントごとの数値しか得られないので、何文字書いたかで位置を計算することになります。

javaの文字の高さは英字中心で、漢字を考慮しているとは言えません。調整する必要があります。

注意すべきなのはdrawString()の位置指定がベースライン、すなわちAscentの下の線であるということです。日本語の最下点はベースラインより下になります。

字を小さくして印字

上図はGimpで作ったものです。最初のbyは英数だけの時の位置ですが、次のbyは国語と合わせて入力した時の位置です。調整されています。困ったことに、この調整はソフトウェアやフォントにより変わる可能性があります。

縦書きの開始位置を指定するときは、Ascentの分だけ下を指定します。文字の高さではありません。

字を小さくして印字

半角文字をはさむと左右の位置が乱れます。文字の中心を揃えて並べるのがよいでしょう。

加えて、縦書きの場合は句読点や括弧などの役物の向きを変換する必要がでる場合があります。今回はこれを考慮しません。

フォントの高さにまつわる値

FontMetricsからいろいろ調べてみます。

FontMetrics fm = g.getFontMetrics();
-----------
fm.getFont().getFontName():Serif.plain //Serifと指定しました。いつものです。
fm.getFont().getName():Serif
fm.getFont().getSize():10
fm.getFont().getSize2D():10.0
fm.getAscent():9   //英字のベースラインから上のことです
fm.getDescent():2  //英字のベースラインから下。pyの出ている長さです
fm.getHeight():11  //ascent+descent なのだと思います
fm.getLeading():0  //行間の間隔だそうです
fm.getMaxAscent():9  //MaxのAscentということで期待しましたがAscentと同じです
fm.getMaxDescent():2  //MaxのDescentということで期待しましたがDescentと同じです
fm.getMaxAdvance():10  //最大有効幅とのことですが、1文字の幅ということでしょう
fm.stringWidth("東a西𠀋南ア北ml"):70  //全角10,半角5で70です

フォントを物理フォントで指定してみました。

fm.getFont()getFontName():IPA P明朝 //"IPA P明朝"と指定しました。
fm.getFont()getName():IPA P明朝
fm.getFont()getSize():10
fm.getFont()getSize2D():10.0
fm.getAscent():9   //変わるところはありません
fm.getDescent():2
fm.getHeight():11
fm.getLeading():0
fm.getMaxAscent():9
fm.getMaxDescent():2
fm.getMaxAdvance():10
fm.stringWidth("東a西𠀋南ア北ml"):72 //!!詳しく調べると、mが9、lが3で +2になっていました。

文字の高さは fm.getHeight() を使うのが順当ですが、ちょっと不安が残ります。行ピッチ(つまりHeight+Leading)を文字の高さとしているのではないかと疑われるフォント設計に出会ったことがあるからです。

縦書き均等割付のプログラム (AjustStringT.java)

AjustStringT.java

package print01;

import java.awt.Graphics2D;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.image.BufferedImage;

public class AjustStringT {
    String kp, nbf;
    int cpct, idxbgn, newcpbgn, nbfcp;
    float fmwm, remm;
    float aidamm = 0f;
    Graphics2D g;
    FontMetrics fm;
    float mm2pt = 72/25.4f;
    float pt2mm = 25.4f/72;
    float dscinpt, ascent; //dscinpt = fm.getDescent());
    boolean debug = false;
    /** wakuhaba に入る mojiretsu を計算 */
    public AjustStringT(Graphics2D g, String mojiretsu, float wakuhaba) {
        this(g,mojiretsu,wakuhaba,0);
    }
    public AjustStringT(Graphics2D g, String mojiretsu, float wakuhaba, int kaishiichi) {
        this.g = g;
        kp = (mojiretsu!=null)? mojiretsu:"";  //nullなら""
        fmwm = (wakuhaba>=0)?wakuhaba:0;       //負なら0
        fm = g.getFontMetrics();
        cpct = kp.codePointCount(0,kp.length());  //kpのcp数
        if (0>kaishiichi) kaishiichi=0;       //負なら0
        if (cpct>kaishiichi){  //cpをindexに換算
            idxbgn = kp.offsetByCodePoints(0,kaishiichi);
            nbf = kp.substring(idxbgn);
        }else{             //開始位置が文字列をはみ出していたら
            idxbgn = kaishiichi-cpct+kp.length();
            nbf = "";
        }
        if(debug)System.out.println("cpct="+cpct+" /kaishiichi="+kaishiichi+" /idxbgn="+idxbgn+" /nbf="+nbf);//test
        nbfcp = nbf.codePointCount(0, nbf.length());
        remm = fmwm - fm.getHeight()*pt2mm*nbfcp; //あまりを計算
        newcpbgn = kaishiichi + nbfcp;  //書いた後の次の文字を計算
        if(debug)System.out.println( "nbf="+nbf+" /remm="+remm +" /nbf.length()="+nbf.length()+" /newcpbgn="+newcpbgn);//test
        while(0>remm && !nbf.isEmpty()) {
            int newendidx = nbf.offsetByCodePoints(nbf.length(), -1); //一つ前のindexを求める
            nbf = nbf.substring(0,newendidx);
            nbfcp = nbf.codePointCount(0, nbf.length());
            remm = fmwm - fm.getHeight()*pt2mm*nbfcp;
            newcpbgn = kaishiichi + nbfcp;
            if(debug)System.out.println( "nbf="+nbf+" /remm="+remm +" /nbf.length()="+nbf.length()+" /newcpbgn="+newcpbgn);//test
        }
        ascent = fm.getAscent();
    }
    public static void main( String[] args ) {
        BufferedImage buffimg = new BufferedImage(400,300,BufferedImage.TYPE_INT_RGB);
        Graphics2D g = buffimg.createGraphics();
        g.setFont(new Font(Font.SERIF, Font.PLAIN, 10));
        float wakuhaba = (args.length>0) ? Float.parseFloat(args[0]):10f;
        int kaishiichi = (args.length>1) ? Integer.parseInt(args[1]):0;
        String mojitachi="東a西𠀋南ア北";
        System.out.println("文字列="+mojitachi+" /枠幅(mm)="+wakuhaba+" /開始番号(0-)="+kaishiichi);
        AjustStringT ast = new AjustStringT(g, mojitachi,wakuhaba,kaishiichi);
    }
    public boolean hasNext(){
        return cpct>newcpbgn;
    }
    public int getNextPt(){
        int retval = -1;
        if (cpct>newcpbgn) retval=newcpbgn;
        return retval;
    }
    public float getLastRemm(){
        return remm;
    }
    /**縦書きを左寄せのように上に詰める。hmを中心線にする */
    public void drawTop(float hm, float vm) {
        float vpp = fm.getHeight();
        int nbflen = nbf.length();
        //int nbfcplen = nbf.codePointCount(0,nbflen);
        int i=0;
        int nexti = 0;
        int cpcti = 0;
        float fontwd;
        while (nbflen>i){
            nexti = nbf.offsetByCodePoints(i,1);
            fontwd = fm.stringWidth(nbf.substring(i,nexti));
            g.drawString(nbf.substring(i,nexti),hm*mm2pt-fontwd/2,vm*mm2pt+vpp*cpcti+ascent);
            i=nexti;
            cpcti++;
        }
    }
    /**縦書きを右寄せのように下に詰める。hmを中心線にする */
    public void drawBottom(float hm, float vm) {
        float vpp = fm.getHeight();
        int nbflen = nbf.length();
        //int nbfcplen = nbf.codePointCount(0,nbflen);
        int i=0;
        int nexti = 0;
        int cpcti = 0;
        float fontwd;
        while (nbflen>i){
            nexti = nbf.offsetByCodePoints(i,1);
            fontwd = fm.stringWidth(nbf.substring(i,nexti));
            g.drawString(nbf.substring(i,nexti),hm*mm2pt-fontwd/2,(vm+remm)*mm2pt+vpp*cpcti+ascent);
            i=nexti;
            cpcti++;
        }
    }
    /**縦の均等割付 hmを中心線にする */
    public void drawTtoB(float hm, float vm) {
        float vpp = fm.getHeight();
        int nbflen = nbf.length();
        int nbfcplen = nbf.codePointCount(0,nbflen);
        if (nbfcplen!=1){
            aidamm = remm/(nbfcplen-1);
            int i=0;
            int nexti = 0;
            int cpcti = 0;
            float fontwd;
            while (nbflen>i){
                nexti = nbf.offsetByCodePoints(i,1);
                fontwd = fm.stringWidth(nbf.substring(i,nexti));
                g.drawString(nbf.substring(i,nexti),hm*mm2pt-fontwd/2,(vm+aidamm*cpcti)*mm2pt+vpp*cpcti+ascent);
                i=nexti;
                cpcti++;
            }
        }else{
            g.drawString(nbf,hm*mm2pt,(vm+remm/2)*mm2pt+ascent);
        }
    }
}

Tsuchi.javaに縦書きの部分を追加する

Tsuchi.javaのprint()に追加します。

Tsuchi.javaに追加

        kp = new AjustString(g2, "科目", hwkm-10f);
        kp.drawKintou(hbas+5f,vthtop+(vtdtop-vthtop)/2+g2.getFontMetrics().getHeight()*pt2mm/2);
        AjustStringT kt;
        kt = new AjustStringT(g2, "単位数" ,vtdtop-vthtop-4f);
        kt.drawTtoB(hbas+hwkm+hwtn/2,vthtop+2f);
        kt = new AjustStringT(g2, "一学期" ,vtdtop-vthtop-4f);
        kt.drawTtoB(hbas+hwkm+hwtn+hptch/2,vthtop+2f);
        kt = new AjustStringT(g2, "二学期" ,vtdtop-vthtop-4f);
        kt.drawTtoB(hbas+hwkm+hwtn+hptch*1.5f,vthtop+2f);
        kt = new AjustStringT(g2, "三学期" ,vtdtop-vthtop-4f);
        kt.drawTtoB(hbas+hwkm+hwtn+hptch*2.5f,vthtop+2f);
        kt = new AjustStringT(g2, "学年" ,vtdtop-vthtop-4f);
        kt.drawTtoB(hbas+hwkm+hwtn+hptch*3.5f,vthtop+2f);

        kt = new AjustStringT(g2, "学年" ,vptch);
        kt.drawTop(hbas+hwkm+hwtn+hptch*2.5f,vtdtop+vptch*(kams.length-2));
        kt.drawBottom(hbas+hwkm+hwtn+hptch*3.5f,vtdtop+vptch*(kams.length-2));

実行結果 その2

上部の単位数部分では均等しか試していないので、下の家庭基礎の欄で、上寄せと下寄せを試している。

通知表の縦書き部分も