SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Javaの標準機能だけで実現する帳票印刷

均等割付・右寄せ・センタリングを簡単に扱う道具を作る

Javaの標準機能だけで実現する帳票印刷 第4回

文字列配置プログラムAdjustString.java

 指定範囲に入りきらない場合に文字列を短くして入る分だけ印字するように改良します。範囲が十分な幅を持つ場合には前のプログラムと同様になります。

十分な幅があればAdjustStringOvfと同じ
十分な幅があればAdjustStringOvfと同じ

 指定範囲が十分な幅を持たない場合の印字例は次のようになります。「AdjustStringOvf.java」と同じで、環境が許せば、【丈`】をサロゲートペアを使う本来の字に入れ替えて同じ印字を得ることができます。

指定範囲が狭すぎる場合のAdjustStringの動作イメージ
指定範囲が狭すぎる場合のAdjustStringの動作イメージ

 プログラムです。

AdjustString.java
/** 
指定範囲に文字列を均等割付、右寄せ、左寄せ、センタリングなどの調整をして描画する
入りきらない場合は、入りきる分だけの部分文字列を保持して
均等割付、左寄せなどの指示により部分文字列を描画する
2019-9-20
@author Adachi
@version 3.0
*/
import java.awt.Graphics2D;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.image.BufferedImage;

public class AdjustString {
    String zstr, str; //motono string, string
    int   zcpct,cpct; //code point count
    int   bgncpi, nxtbgncpi; //cp index begin, next cp index begin
    float wdmm;  //width in mm
    float remm  = 0f;  //remain in mm;
    float gapmm = 0f;
    Graphics2D g2;
    FontMetrics fm;
    float mm2pt = 72/25.4f;
    float pt2mm = 25.4f/72;
    boolean debug = true;
    /** areawidth に string を入れる時の余り remm を計算 */
    public AdjustString(Graphics2D g2, String string, float areawidth) {
        this(g2,string,areawidth,0);
    }
    /** areawidth に string を入れる時の余り remm を計算。ただし str の bgncpiから */
    public AdjustString(Graphics2D g2, String string, float areawidth, int begincpindex) {
        this.g2 = g2;
        fm = g2.getFontMetrics();
        zstr = (string!=null) ? string : "";  //nullなら""
        wdmm = (areawidth>=0f) ? areawidth : 0f;       //負なら0
        zcpct = zstr.codePointCount(0,zstr.length());  //zstrのcp数
        bgncpi = (begincpindex>=0) ? begincpindex : 0;       //負なら0
        if (zcpct>bgncpi){  //開始位置をindexに換算
            int idxbgn = zstr.offsetByCodePoints(0,bgncpi);
            str = zstr.substring(idxbgn);
        }else{             //開始位置が文字列をはみ出していたら
            str = "";
        }
        remm = wdmm - fm.stringWidth(str)*pt2mm;  //あまりを計算
        if(debug){System.out.println(report());}
        while(0>remm && !str.isEmpty()) {
            int newendidx = str.offsetByCodePoints(str.length(), -1); //一つ前のindexを求める
            str = str.substring(0,newendidx);
            remm = wdmm - fm.stringWidth(str)*pt2mm;
            if(debug){System.out.println(report());}
        }
        cpct = str.codePointCount(0,str.length());
        nxtbgncpi = bgncpi + cpct;
        gapmm = remm/(cpct-1);
    }
    /** 文字列・余り・文字数 をレポートする。(デバッグ用) */
    public String report(){
        cpct = str.codePointCount(0,str.length());
        nxtbgncpi = bgncpi + cpct;
        String rstr = String.format("str=%s /remm=%f /str.length()=%d /cpct=%d /nxtbgncpi=%d",
                                     str,    remm,    str.length(),    cpct,    nxtbgncpi);
        return rstr;
    }

    /** zstr のなかで書ききれなかった残りがあるかを答える */
    public boolean hasNext(){
        return zcpct>nxtbgncpi;
    }
    /** 書ききれなかった残りの文字の開始位置(コードポイントで)を答える */
    public int getNextPt(){
        int retval = -1;
        if (zcpct>nxtbgncpi) retval=nxtbgncpi;
        return retval;
    }
    /** strがareawidth のなかに収まるかを答える */
    public boolean isFitInto(){
        return remm>=0;
    }
    /** 文字列の幅を引いたあまりをmmで返す */
    public float getRemain(){
        return remm;
    }
    /** 文字間の隙間をmmで返す */
    public float getGap(){
        return gapmm;
    }
    /** 書けるだけの文字列を左寄せで描画する */
    public void drawLeft(float hm, float vm) {
        g2.drawString(str,hm*mm2pt,vm*mm2pt);
    }
    /** 書けるだけの文字列を右寄せで描画する */
    public void drawRight(float hm, float vm) {
        g2.drawString(str,(hm+remm)*mm2pt,vm*mm2pt);
    }
    /** 書けるだけの文字列をセンタリングで描画する */
    public void drawCenter(float hm, float vm) {
        g2.drawString(str,(hm+remm/2)*mm2pt,vm*mm2pt);
    }
    /** 書けるだけの文字列を均等割付で描画する */
    public void drawKintou(float hm, float vm) {
        float hpp = 0;
        int strlen = str.length();
        if (cpct>1){
            int i=0;
            int nexti = 0;
            int ct = 0;
            while (strlen>i){ 
                nexti = str.offsetByCodePoints(i,1);
                g2.drawString(str.substring(i,nexti),(hm+gapmm*ct)*mm2pt+hpp,vm*mm2pt);
                hpp = fm.stringWidth(str.substring(0,nexti)); //pt
                i=nexti;
                ct++;
            }
        }else{
            drawCenter(hm, vm);
        }
    }
    public static void main( String[] args ) {
        BufferedImage buffimg = new BufferedImage(800,600,BufferedImage.TYPE_INT_RGB);
        Graphics2D g2 = buffimg.createGraphics();
        float w = (args.length>0) ? Float.parseFloat(args[0]):50f;
        int bgncpi = (args.length>1) ? Integer.parseInt(args[1]):0;
        String str="東西【丈`】南北";
        System.out.println("文字列="+str+" /枠幅(mm)="+w+" /開始番号(0-)="+bgncpi);
        float x = 30.1f;
        float y = 30.2f;
        float h = 16.3f;
        AdjustString ads = new AdjustString(g2, str,w,bgncpi);
        System.out.println("--main--");
        System.out.println(ads.report());
        System.out.println(ads.getGap());
        ads.drawKintou(x,y+h);
    }
}

 コンストラクタ以外は「AdjustStringOvf.java」とほとんど違いはありません。コンストラクタではまず書きれなかった時に一文字ずつ減らして書ききれる分だけの文字列(str)を保持するようにします。後のdrawKintou()などのメソッドではこのstrを印字対象にします。

 最初の実行例は引数に10を渡して10mm中に書ける文字列(str)を決めるまでの過程を見ています。remmが正の数になるまで文字を減らしてついに2文字にします。--main--までの出力がそれです。これは動作確認のために出しているもので、実際の運用では boolean debug = true;をfalseにすることにより抑制します。

--main--から下の出力は、AdjustString のコンストラクタを呼び出した後のものです。AdjustString のインスタンスadsから情報を得たあと、adsインスタンス内の(str)を均等割付けで印字させています。ただし、このmain()ではBufferedImageに描画するだけで実際の印刷はしません。

adachi@debian64:~$ java AdjustString 10
文字列=東西【丈`】南北 /枠幅(mm)=10.0 /開始番号(0-)=0
str=東西【丈`】南北 /remm=-11.166666 /str.length()=6 /cpct=5 /nxtbgncpi=5
str=東西【丈`】南 /remm=-6.933334 /str.length()=5 /cpct=4 /nxtbgncpi=4
str=東西【丈`】 /remm=-2.700000 /str.length()=4 /cpct=3 /nxtbgncpi=3
str=東西 /remm=1.533333 /str.length()=2 /cpct=2 /nxtbgncpi=2
--main--
str=東西 /remm=1.533333 /str.length()=2 /cpct=2 /nxtbgncpi=2
1.5333328

 つぎに、文字列の残りの部分を書くことを想定して、引数を「10 2」として再び実行したものを以下に示します。「10 2」は10mm中に、2番目の文字(0から数える)から実行するという意味です。やはり、--main--までの出力でわかるとおり、remmが正の数になるまで文字を減らしてついに2文字にします。ただし、【丈`】がサロゲートペアからなる一文字であることに注意してください。

adachi@debian64:/media/adachi/S1T/java/print04$ java AdjustString 10 2
str=【丈`】南北 /remm=-2.700000 /str.length()=4 /cpct=3 /nxtbgncpi=5
str=【丈`】南 /remm=1.533333 /str.length()=3 /cpct=2 /nxtbgncpi=4
--main--
str=【丈`】南 /remm=1.533333 /str.length()=3 /cpct=2 /nxtbgncpi=4
1.5333328

AdjustString.javaを使って帳票を印刷する例

 前項のPrintableWithAdjustStringOvf.javaの中のAdjustStringOvfをAdjustStringに替えると同様の印字結果が得られますが、それは省略して、書ききれない場合は2行にするというのを試してみます。

 使い方の骨子は次のようなものです。hasNext()がtrueを返す場合は、入りきらないので、左寄せで書けるだけ書き、getNextPt()で残された文字列の位置を得て、それを引数に新たなインスタンスを作り、今度は右寄せにします。

AdjustString ads = new AdjustString(g2,str,w);
if(ads.hasNext()){
    ads.drawLeft(x,y-charh/2);
    ads = new AdjustString(g2,str,w,ads.getNextPt());
    ads.drawRight(x,y+charh/2);
}else{
    ads.drawKintou(x,y);
}

 印字が重ならないようにcharh/2を引いたり、足したりしていますが、charhは文字の高さです。この例では2行以上になることはないという仮定で考えています。

 プログラムです。

PrintableWithAdjustString2.java
import java.awt.*;
import java.awt.print.*;
import javax.print.*;
import javax.print.attribute.*;
import javax.print.attribute.standard.*;

/** PrintableWithAdjustString2.java 
 AdjustString.javaを使い、均等割付印刷をします。
 1行に入りきらない時は2行に分けます。
 2019-05-26 adachi
*/
public class PrintableWithAdjustString2 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(Font.SERIF, Font.PLAIN, 36));
        float mm2pt = 72/25.4f;
        String str1="東西【丈`】南北";
        String str2="黒茶赤橙黃緑青紫灰白";
        String str3="鱈鮭鰯【魚花】";
        float x = 20.1f;
        float y = 20.2f;
        float w = 100f;
        float h = 24.3f;
        float padx = 3f;
        float pady = 2f;
        float charh = g2.getFontMetrics().getFont().getSize()/mm2pt;
        g2.setColor(Color.BLACK);
        AdjustString ads = new AdjustString(g2,str1,w-padx*2);
        ads.drawKintou(x+padx,y+h-pady);
        ads =  new AdjustString(g2,str2,w-padx*2);
        if(ads.hasNext()){  //長い文字列を2行に
            ads.drawLeft(x+padx,y+2*h-pady-charh/2);
            ads = new AdjustString(g2, str2 ,w-padx*2, ads.getNextPt());
            ads.drawRight(x+padx,y+2*h-pady+charh/2);
        }else{
            ads.drawKintou(x+padx,y+2*h-pady);
        }
        ads =  new AdjustString(g2,str3,w-padx*2);
        ads.drawKintou(x+padx,y+3*h-pady);
        return PAGE_EXISTS;
    }

    public static void main(String[] args) {
        Printable pable = new PrintableWithAdjustString2();
        PrintRequestAttributeSet rqset = new HashPrintRequestAttributeSet();
        MediaSizeName msname = MediaSizeName.ISO_A4; //A4用紙
        rqset.add(msname);
        MediaSize msize = MediaSize.getMediaSizeForName(msname);
        float mwidth  = msize.getX(MediaPrintableArea.MM);
        float mheight = msize.getY(MediaPrintableArea.MM);
        float L = 10.1f;
        float R = 10.2f;
        float T = 10.3f;
        float B = 10.4f;
        rqset.add(new MediaPrintableArea(
            L, T, (mwidth - L - R), (mheight - T - B),
            MediaPrintableArea.MM));
        PrinterJob pj = PrinterJob.getPrinterJob();
        pj.setPrintable(pable);
        if (pj.printDialog(rqset)) {
            try { pj.print(rqset); }
            catch (PrinterException e) {
                System.err.println(e);
            }
        }
    }
}

 実行結果です。"黒茶赤橙黃緑青紫灰白"が入らないので入る分を左寄せにし、残りを右寄せにして、均等割付けの左右に合わせます。

指定範囲が狭くて2行にする場合のAdjustStringの動作イメージ
指定範囲が狭くて2行にする場合のAdjustStringの動作イメージ

 この3つの文字列の中で、"【丈`】"と"【魚花】"(注2)がサロケートペアの文字です。

注2

 CodeZineのサイトではCMSの仕様でサロゲートペアの文字が扱えないため、「U+29e3d」の文字(ほっけ)を【魚花】(実際は1文字)と表記しています。

 debug=trueのままなので、文字列調整の端末に途中経過がでます。実際の運用ではdebug=falseにしておくのが良いでしょう。

adachi@debian64:~$ java PrintableWithAdjustString2
str=東西【丈`】南北 /remm=30.500000 /str.length()=6 /cpct=5 /nxtbgncpi=5
str=黒茶赤橙黃緑青紫灰白 /remm=-33.000000 /str.length()=10 /cpct=10 /nxtbgncpi=10
str=黒茶赤橙黃緑青紫灰 /remm=-20.300003 /str.length()=9 /cpct=9 /nxtbgncpi=9
str=黒茶赤橙黃緑青紫 /remm=-7.599998 /str.length()=8 /cpct=8 /nxtbgncpi=8
str=黒茶赤橙黃緑青 /remm=5.099998 /str.length()=7 /cpct=7 /nxtbgncpi=7
str=紫灰白 /remm=55.900002 /str.length()=3 /cpct=3 /nxtbgncpi=10
str=鱈鮭鰯【魚花】 /remm=43.200001 /str.length()=5 /cpct=4 /nxtbgncpi=4

次のページ
入りきらない時は小さい字にする

この記事は参考になりましたか?

Javaの標準機能だけで実現する帳票印刷連載記事一覧

もっと読む

この記事の著者

安達 順一(アダチ ジュンイチ)

私立高校に理科・情報の教員として勤めていました。Linuxサーバー/クライアントの授業システムを作り、移動プロファイルで運用していました。教員用にもLinuxサーバーを用意し成績処理プログラムを書きました。情報の学校設定科目ではウェブページ制作とjavaのプログラミングの初歩の授業を作りました。情報...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/12449 2020/06/29 11:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー