今日もプログラミング

IT技術とかプログラミングのこととか特にJavaを中心に書いていきます

POIで正方形を描画する

ちょっと間が空いてしまったが、この前の続き。

今回はPOIを使って、正方形を描いてみたい。

テキストボックスを描くのは簡単だが、問題は座標だ。

 

POIの座標指定

POIでテキストボックスを描くには、まず

Sheet#createDrawingPatriarch() 

により、XSSFDrawingインスタンスを取得する(戻り値の型はDrawingだが、ダウンキャストできる)。そして、

XSSFDrawing#createTextbox(XSSFClientAnchor)

によりテキストボックスを追加する。このXSSFClientAnchorが座標を表している。で、こいつを得るには、

XSSFDrawing#createAnchor(int dx1, int dy1, int dx2, int dy2, int col1, int row1, int col2, int row2)

を呼ぶ。このcol1とかcol2は列のインデックス、row1とかrow2は行のインデックス、そしてdx1、dx2、dy1、dy2はセル内のX座標やY座標だ。

つまり、シートの左上からの絶対座標ではなく、セルの位置+セル内の相対座標ということだ。

 

確かに、Excelで列幅とか行の高さを変えると、それにまたがる図形の大きさも変わる。

それはそれで便利なのだが、セルとは関係なく絶対座標で図形を描きたい場合にはちょっと面倒だ。

 

POIで作成したExcelブックのフォントサイズは?

絶対座標をXSSFClientAnchorに変換するには、列の幅や行の高さが必要だ。

この前調べたように、デフォルトの列の幅や行の高さはフォントサイズに依存する。

 

POIでExcelブックを作成すると、フォントサイズは11ポイントになっている。

これは、自分の環境ではExcelのデフォルトフォントサイズと同じだ。

では、Excelのデフォルトフォントサイズを変えるとどうなるか?

「オプション」→「基本設定」で、「新しいブックの作成時」の「フォントサイズ」を16にして、再度POIでブックを作成してみる。

すると…、やはり11ポイントだった。

 

Excelの設定は反映されないのかな?

デバッガを使いつつソースを追ってみると…、

new XSSFWorkbook (org.apache.poi.xssf.usermodel)
↓
XSSFWorkbook#onWorkbookCreate
↓
...
↓
new StylesTable (org.apache.poi.xssf.model)
↓
StylesTable#initialize
↓
StylesTable#createDefaultFont
↓
XSSFFont#setFontHeightInPoints(XSSFFont.DEFAULT_FONT_SIZE)

のように呼んでいて、この定数の値は11だった。

ということで、Excelの設定にかかわらず11ポイントになるということだな。

だったら、列の幅とか行の高さのデフォルト値は、決め打ちでいいかな…。

 

X座標の計算

マジックナンバーだらけだが…、以下のようなメソッドを作ってみた。

    private static int[] getAnchorX(Sheet sheet, int xInPixel) {
        int xInEMU = xInPixel * XSSFShape.EMU_PER_PIXEL;
        int currentInXEMU = 0;

        Row row = sheet.getRow(0);
        if (row == null) {
            row = sheet.createRow(0);
        }
        for (int columnNum = 0;; columnNum++) {
            if (row.getCell(columnNum) == null) {
                row.createCell(columnNum);
            }

            if (sheet.getColumnWidth(columnNum) == sheet.getDefaultColumnWidth() * 256) {
                // Excelのデフォルト値は違うので明示的に設定する
                sheet.setColumnWidth(columnNum, 9 * 256);
            }

            // Excelでは256columnWidthが8ピクセル
            int columnWidthInEMU = sheet.getColumnWidth(columnNum) / 256 * 8 * XSSFShape.EMU_PER_PIXEL;
            if (xInEMU < currentInXEMU + columnWidthInEMU) {
                return new int[]{columnNum, xInEMU - currentInXEMU};
            }
            currentInXEMU += columnWidthInEMU;
        }
    }

絶対X座標を渡すと、列のインデックスとセル内の相対X座標が返る。

(本当はそれ用のクラスを作って返した方がいいんだけど、サンプルなので)

順番に列幅を計算しながら、X座標から引いていく。引けなくなった時点で値を返せばよい。

 

デフォルトの列幅の処理が気持ち悪いが…、この前調べたように、POIのデフォルトの列幅がExcelと違っているためで、仕方ない…。

 

Y座標の計算

こちらも似たような感じ。

    private static int[] getAnchorY(Sheet sheet, int yInPixel) {
        int yInEMU = yInPixel * XSSFShape.EMU_PER_PIXEL;
        int currentYInEMU = 0;

        for (int rowNum = 0;;rowNum++) {
            Row currentRow = sheet.getRow(rowNum);
            if (currentRow == null) {
                currentRow = sheet.createRow(rowNum);
            }

            if (currentRow.getHeight() == sheet.getDefaultRowHeight()) {
                // Excelのデフォルト値は違うので明示的に設定する
                currentRow.setHeightInPoints(13.5f);
            }

            // 行の高さ(EMU)
            // ※row.getHeight()は1/20ポイント単位
            int rowHeightInEMU = (int)(currentRow.getHeight() / 20.0D * XSSFShape.EMU_PER_POINT);
            if (yInEMU < currentYInEMU + rowHeightInEMU) {
                return new int[]{rowNum, (yInEMU - currentYInEMU)};
            }
            currentYInEMU += rowHeightInEMU;
        }
    }

 

POIで正方形を描く

ここまでできれば、後は簡単。

100ピクセル×100ピクセルの正方形を描いてみる。

    try (Workbook workbook = new XSSFWorkbook()) {
        Sheet sheet = workbook.createSheet();
        XSSFDrawing drawing = (XSSFDrawing)sheet.createDrawingPatriarch();

        int[] startX = getAnchorX(sheet, 100);
        int[] endX = getAnchorX(sheet, 200);
        int[] startY = getAnchorY(sheet, 100);
        int[] endY = getAnchorY(sheet, 200);

        XSSFClientAnchor anchor = drawing.createAnchor(startX[1], startY[1], endX[1], endY[1], startX[0], startY[0], endX[0], endY[0]);
        XSSFTextBox textBox = drawing.createTextbox(anchor);
        textBox.setText("Hello World!");
        textBox.setLineWidth(1);
        textBox.setLineStyleColor(255, 0, 0);

        File file = new File("test.xlsx");
        try (OutputStream out = new FileOutputStream(file)) {
            workbook.write(out);
        }
    }

 

できたExcelブックはこれ。

f:id:hito4_t:20150330220031p:plain

 

本当に正方形になっているのかな?と思って、ペイントで100×100の正方形を描いて、隣に置いてみた。

ぴったり!

f:id:hito4_t:20150330220035p:plain

 

でも、マジックナンバーとかデフォルト値の処理とか、汚らしいソースになってしまった…。

もっときれいにできる方法は無いのかなあ。

 

OS Windows 7
Office 2010
Java 8u31
Apache POI 3.11