Apache POIでExcelの図形の(拡張?)書式設定を変更する
Apache POIを使ってExcelの図形(シェイプ)を作って 図形の書式設定を変えることは、一部の書式は変更することはできますが、それ以外の書式設定を変えるにはどうすれば良いのか、ちょっと調べてみました。
今回の結論はリフレクションを使ったものになります。正規の方法では無いと思いますので、ご利用の際にはご注意ください。
追記(2015-06-06): XSSFバージョン(Excel2007以降のファイルフォーマット、*.xlsxの形式)を最後に追記しました。こちらはリフレクション無しです。
はじめに
とあるお題にて、「Apache POIで直線の矢印を設定したい」というものがありました。
図形の線の色、線のスタイル、線の幅、塗りつぶし色を設定するには、HSSFShape
にメソッドが用意されているので、それを使って変更することができます。
しかし、HSSFShape
のメソッドで提供されている図形の書式設定は、Excelの図形の書式設定のごく一部だけです。
私はPOIを使ったことがありましたが、ほとんどが読み取りの処理で、図形の処理はまったくやったことがありませんでした。
今回はちょっと興味が湧いたので、少しだけ調べてみました。
普通の使い方で設定可能な書式を設定してみる
まず最初に、新しいブックを作って図形を追加し、保存するプログラムを示します。
今回は、Excel97-2003ブックで作成します。
// import java.io.*; // import java.nio.file.*; // import org.apache.poi.hssf.usermodel.*; Path path = Paths.get("shape.xls"); try (OutputStream os = Files.newOutputStream(path); HSSFWorkbook book = new HSSFWorkbook()) { HSSFSheet sheet = book.createSheet(); HSSFPatriarch patriarch = sheet.createDrawingPatriarch(); // 図形1: 線1 HSSFSimpleShape line1 = patriarch.createSimpleShape(patriarch.createAnchor(100, 100, 0, 0, 0, 0, 3, 10)); line1.setShapeType(HSSFSimpleShape.OBJECT_TYPE_LINE); line1.setLineWidth(205 * 127); // 線の幅 2.05pt line1.setLineStyle(HSSFShape.LINESTYLE_LONGDASHGEL); // 長破線 line1.setLineStyleColor(255, 0, 0); // 線の色 赤 // 図形2: 四角形1 HSSFSimpleShape rect1 = patriarch.createSimpleShape(patriarch.createAnchor(100, 100, 0, 0, 4, 0, 8, 10)); rect1.setShapeType(HSSFSimpleShape.OBJECT_TYPE_RECTANGLE); rect1.setFillColor(0, 0, 255); // 塗りつぶし 青 rect1.setLineWidth(542 * 127); // 線の幅 5.42pt book.write(os); } catch (IOException e) { throw new UncheckedIOException(e); }
Excelで見てみると、こんな感じになります。

図1:線と四角形を作ってそれぞれスタイルを変更
前述のとおり、「ふつー」に設定できる書式設定はごく一部です。
ほかの書式設定はどうすれば良いのでしょうか。
EscherProperty
の設定について
Excelに限らず、MS Officeの図形の書式はMicrosoft Office Drawing formatに基づいています。POIの中では、org.apache.poi.ddf
パッケージにこのフォーマットを扱うAPIがそろっています。
ググったりソースコードをちょっと調べた感じでは、このパッケージにあるEscherProperty (POI API Documentation)
クラスを使うとできそうです。
ところが、HSSFShape
にはsetPropertyValue(EscherProperty)
メソッドがあるものの、protectedなので直接呼び出すことができません。
HSSFShape
かHSSFSimpleShape
を継承したクラスを作ったり、HSSFPatriarch
クラスはfinalなのでクローンを作ってみるのを検討したりしましたが、パッケージプライベートのメソッドが障碍になって上手くいきませんでした。
リフレクションでEscherProperty
を設定する
リフレクションでEscherProperty
を設定してみます。
下記コードは、直線の終点を矢印にするプロパティーを設定する例です。
- 直線の終点を矢印にする
// import java.lang.reflect.*; // 追加 EscherProperty lineEndArrowHeadIs1 = new EscherSimpleProperty(EscherProperties.LINESTYLE__LINEENDARROWHEAD, 1); try { Method m = HSSFShape.class.getDeclaredMethod("setPropertyValue", EscherProperty.class); m.setAccessible(true); m.invoke(line1, lineEndArrowHeadIs1); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); }
毎回これを書くのは煩雑なので、関数型インターフェイスを使ってユーティリティー化してみましょう。
- リフレクション+関数型インターフェイスで
EscherProperty
を設定
// import java.io.*; // import java.lang.reflect.*; // import java.nio.file.*; // import java.util.function.*; // import org.apache.poi.ddf.*; // import org.apache.poi.hssf.usermodel.*; Method m; try { m = HSSFShape.class.getDeclaredMethod("setPropertyValue", EscherProperty.class); m.setAccessible(true); } catch (NoSuchMethodException | SecurityException e) { throw new RuntimeException(e); } BiConsumer<HSSFShape, EscherProperty> funcSetProp = (shape, prop) -> { try { m.invoke(shape, prop); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new RuntimeException(e); } }; Path path = Paths.get("shape.xls"); try (OutputStream os = Files.newOutputStream(path); HSSFWorkbook book = new HSSFWorkbook()) { HSSFSheet sheet = book.createSheet(); HSSFPatriarch patriarch = sheet.createDrawingPatriarch(); // 図形1: 線1 HSSFSimpleShape line1 = patriarch.createSimpleShape(patriarch.createAnchor(100, 100, 0, 0, 0, 0, 3, 10)); line1.setShapeType(HSSFSimpleShape.OBJECT_TYPE_LINE); line1.setLineWidth(205 * 127); // 線の幅 2.05pt line1.setLineStyle(HSSFShape.LINESTYLE_LONGDASHGEL); // 長破線 line1.setLineStyleColor(255, 0, 0); // 線の色 赤 // 終点 鋭い矢印 サイズ7 funcSetProp.accept(line1, new EscherSimpleProperty(EscherProperties.LINESTYLE__LINEENDARROWHEAD, 1)); funcSetProp.accept(line1, new EscherSimpleProperty(EscherProperties.LINESTYLE__LINEENDARROWWIDTH, 2)); funcSetProp.accept(line1, new EscherSimpleProperty(EscherProperties.LINESTYLE__LINEENDARROWLENGTH, 0)); // 図形2: 四角形1 HSSFSimpleShape rect1 = patriarch.createSimpleShape(patriarch.createAnchor(100, 100, 0, 0, 4, 0, 8, 10)); rect1.setShapeType(HSSFSimpleShape.OBJECT_TYPE_RECTANGLE); rect1.setFillColor(0, 0, 255); // 塗りつぶし 青 rect1.setLineWidth(542 * 127); // 線の幅 5.42pt // 影 funcSetProp.accept(rect1, new EscherSimpleProperty(EscherProperties.SHADOWSTYLE__SHADOWOBSURED, 196610)); funcSetProp.accept(rect1, new EscherSimpleProperty(EscherProperties.SHADOWSTYLE__OFFSETX, 233487)); funcSetProp.accept(rect1, new EscherSimpleProperty(EscherProperties.SHADOWSTYLE__OFFSETY, 233487)); book.write(os); } catch (IOException e) { throw new UncheckedIOException(e); }

図2:
EscherProperty
で書式設定を追加したもの
矢印と影が設定できました。
リバースエンジニアリング
EscherProperty
を図形に設定する方法は分かりましたが、EscherProperty
に設定する値が分かりません。LINESTYLE__LINEENDARROWHEAD
のようなプロパティーは当てずっぽうでもできましたが、全部のプロパティーを調べるのは大変です。
そこで、既にExcel上で設定された図形の書式設定を読み取って出力するプログラムも作ってみました。こちらもリフレクション+関数型インターフェイスを使っています。
- Excelの図形の書式設定を出力
// import java.io.*; // import java.lang.reflect.*; // import java.nio.file.*; // import java.util.function.*; // import org.apache.poi.ddf.*; // import org.apache.poi.hssf.usermodel.*; // 図形名 String[] shapeTypeNames = { "NotPrimitive", "Rectangle", "RoundRectangle", "Ellipse", "Diamond", "IsocelesTriangle", "RightTriangle", "Parallelogram", "Trapezoid", "Hexagon", "Octagon", "Plus", "Star", "Arrow", "ThickArrow", "HomePlate", "Cube", "Balloon", "Seal", "Arc", "Line", "Plaque", "Can", "Donut", "TextSimple", "TextOctagon", "TextHexagon", "TextCurve", "TextWave", "TextRing", "TextOnCurve", "TextOnRing", "StraightConnector1", "BentConnector2", "BentConnector3", "BentConnector4", "BentConnector5", "CurvedConnector2", "CurvedConnector3", "CurvedConnector4", "CurvedConnector5", "Callout1", "Callout2", "Callout3", "AccentCallout1", "AccentCallout2", "AccentCallout3", "BorderCallout1", "BorderCallout2", "BorderCallout3", "AccentBorderCallout1", "AccentBorderCallout2", "AccentBorderCallout3", "Ribbon", "Ribbon2", "Chevron", "Pentagon", "NoSmoking", "Star8", "Star16", "Star32", "WedgeRectCallout", "WedgeRRectCallout", "WedgeEllipseCallout", "Wave", "FoldedCorner", "LeftArrow", "DownArrow", "UpArrow", "LeftRightArrow", "UpDownArrow", "IrregularSeal1", "IrregularSeal2", "LightningBolt", "Heart", "PictureFrame", "QuadArrow", "LeftArrowCallout", "RightArrowCallout", "UpArrowCallout", "DownArrowCallout", "LeftRightArrowCallout", "UpDownArrowCallout", "QuadArrowCallout", "Bevel", "LeftBracket", "RightBracket", "LeftBrace", "RightBrace", "LeftUpArrow", "BentUpArrow", "BentArrow", "Star24", "StripedRightArrow", "NotchedRightArrow", "BlockArc", "SmileyFace", "VerticalScroll", "HorizontalScroll", "CircularArrow", "NotchedCircularArrow", "UturnArrow", "CurvedRightArrow", "CurvedLeftArrow", "CurvedUpArrow", "CurvedDownArrow", "CloudCallout", "EllipseRibbon", "EllipseRibbon2", "FlowChartProcess", "FlowChartDecision", "FlowChartInputOutput", "FlowChartPredefinedProcess", "FlowChartInternalStorage", "FlowChartDocument", "FlowChartMultidocument", "FlowChartTerminator", "FlowChartPreparation", "FlowChartManualInput", "FlowChartManualOperation", "FlowChartConnector", "FlowChartPunchedCard", "FlowChartPunchedTape", "FlowChartSummingJunction", "FlowChartOr", "FlowChartCollate", "FlowChartSort", "FlowChartExtract", "FlowChartMerge", "FlowChartOfflineStorage", "FlowChartOnlineStorage", "FlowChartMagneticTape", "FlowChartMagneticDisk", "FlowChartMagneticDrum", "FlowChartDisplay", "FlowChartDelay", "TextPlainText", "TextStop", "TextTriangle", "TextTriangleInverted", "TextChevron", "TextChevronInverted", "TextRingInside", "TextRingOutside", "TextArchUpCurve", "TextArchDownCurve", "TextCircleCurve", "TextButtonCurve", "TextArchUpPour", "TextArchDownPour", "TextCirclePour", "TextButtonPour", "TextCurveUp", "TextCurveDown", "TextCascadeUp", "TextCascadeDown", "TextWave1", "TextWave2", "TextWave3", "TextWave4", "TextInflate", "TextDeflate", "TextInflateBottom", "TextDeflateBottom", "TextInflateTop", "TextDeflateTop", "TextDeflateInflate", "TextDeflateInflateDeflate", "TextFadeRight", "TextFadeLeft", "TextFadeUp", "TextFadeDown", "TextSlantUp", "TextSlantDown", "TextCanUp", "TextCanDown", "FlowChartAlternateProcess", "FlowChartOffpageConnector", "Callout90", "AccentCallout90", "BorderCallout90", "AccentBorderCallout90", "LeftRightUpArrow", "Sun", "Moon", "BracketPair", "BracePair", "Star4", "DoubleWave", "ActionButtonBlank", "ActionButtonHome", "ActionButtonHelp", "ActionButtonInformation", "ActionButtonForwardNext", "ActionButtonBackPrevious", "ActionButtonEnd", "ActionButtonBeginning", "ActionButtonReturn", "ActionButtonDocument", "ActionButtonSound", "ActionButtonMovie", "HostControl", "TextBox", }; // HSSFShape#getOptRecordアクセッサー Method m; try { m = HSSFShape.class.getDeclaredMethod("getOptRecord"); m.setAccessible(true); } catch (NoSuchMethodException | SecurityException e) { throw new RuntimeException(e); } Function<HSSFShape, EscherOptRecord> funcGetOptRecord = (shape) -> { try { return (EscherOptRecord)m.invoke(shape); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new RuntimeException(e); } }; // メイン部分 Path path = Paths.get("shape.xls"); try (InputStream is = Files.newInputStream(path); HSSFWorkbook book = new HSSFWorkbook(is)) { HSSFSheet sheet = book.getSheetAt(0); System.out.printf("sheet name = [%s]%n", sheet.getSheetName()); HSSFPatriarch patriarch = sheet.createDrawingPatriarch(); for (HSSFShape shape : patriarch.getChildren()) { HSSFSimpleShape ss = (HSSFSimpleShape)shape; System.out.println("* shape type = " + shapeTypeNames[ss.getShapeType()]); EscherOptRecord r = funcGetOptRecord.apply(shape); for (EscherProperty prop : r.getEscherProperties()) { System.out.println(">>> " + prop); } } } catch (IOException e) { throw new UncheckedIOException(e); }
- 結果
sheet name = [Sheet0] * shape type = Line >>> propNum: 324, RAW: 0x0144, propName: geometry.shapepath, complex: false, blipId: false, value: 4 (0x00000004) >>> propNum: 385, RAW: 0x0181, propName: fill.fillcolor, complex: false, blipId: false, value: 134217737 (0x08000009) >>> propNum: 447, RAW: 0x01BF, propName: fill.nofillhittest, complex: false, blipId: false, value: 65536 (0x00010000) >>> propNum: 448, RAW: 0x01C0, propName: linestyle.color, complex: false, blipId: false, value: 255 (0x000000FF) >>> propNum: 459, RAW: 0x01CB, propName: linestyle.linewidth, complex: false, blipId: false, value: 26035 (0x000065B3) >>> propNum: 462, RAW: 0x01CE, propName: linestyle.linedashing, complex: false, blipId: false, value: 7 (0x00000007) >>> propNum: 465, RAW: 0x01D1, propName: linestyle.lineendarrowhead, complex: false, blipId: false, value: 1 (0x00000001) >>> propNum: 468, RAW: 0x01D4, propName: linestyle.lineendarrowwidth, complex: false, blipId: false, value: 2 (0x00000002) >>> propNum: 469, RAW: 0x01D5, propName: linestyle.lineendarrowlength, complex: false, blipId: false, value: 0 (0x00000000) >>> propNum: 471, RAW: 0x01D7, propName: linestyle.lineendcapstyle, complex: false, blipId: false, value: 0 (0x00000000) >>> propNum: 511, RAW: 0x01FF, propName: linestyle.nolinedrawdash, complex: false, blipId: false, value: 524296 (0x00080008) >>> propNum: 959, RAW: 0x03BF, propName: groupshape.print, complex: false, blipId: false, value: 524288 (0x00080000) * shape type = Rectangle >>> propNum: 324, RAW: 0x0144, propName: geometry.shapepath, complex: false, blipId: false, value: 4 (0x00000004) >>> propNum: 385, RAW: 0x0181, propName: fill.fillcolor, complex: false, blipId: false, value: 16711680 (0x00FF0000) >>> propNum: 447, RAW: 0x01BF, propName: fill.nofillhittest, complex: false, blipId: false, value: 65536 (0x00010000) >>> propNum: 448, RAW: 0x01C0, propName: linestyle.color, complex: false, blipId: false, value: 134217792 (0x08000040) >>> propNum: 459, RAW: 0x01CB, propName: linestyle.linewidth, complex: false, blipId: false, value: 68834 (0x00010CE2) >>> propNum: 462, RAW: 0x01CE, propName: linestyle.linedashing, complex: false, blipId: false, value: 0 (0x00000000) >>> propNum: 511, RAW: 0x01FF, propName: linestyle.nolinedrawdash, complex: false, blipId: false, value: 524296 (0x00080008) >>> propNum: 517, RAW: 0x0205, propName: shadowstyle.offsetx, complex: false, blipId: false, value: 233487 (0x0003900F) >>> propNum: 518, RAW: 0x0206, propName: shadowstyle.offsety, complex: false, blipId: false, value: 233487 (0x0003900F) >>> propNum: 575, RAW: 0x023F, propName: shadowstyle.shadowobsured, complex: false, blipId: false, value: 196610 (0x00030002) >>> propNum: 959, RAW: 0x03BF, propName: groupshape.print, complex: false, blipId: false, value: 524288 (0x00080000)
この出力結果を元に、EscherProperty
の値を決めていけばOKです。
おわりに
設定することはできましたが、冒頭でも述べているとおり、これが正規の方法ではありません。
もっと良い方法がありましたら、是非お教えください。
追記(2015-06-06): XSSFバージョン
XSSFは、Excel2007以降の*.xlsxファイルとして保存されるOpen XML Formatsに対応したAPIです。
メインの部分はHSSFと大体同じですが、図形の拡張属性の変更はEscherProperty
ではなく、CTXXXProperties
(XXX
の部分は種類によって変わる)を使って設定します。また、こちらのプロパティーは、リフレクション無しで扱えます。ただし階層が少し複雑です。
なお、XSSFなどのOpen XML FormatsのAPIが含まれるpoi-ooxml
が必要です。
- XSSFで直線の矢印と
影付き四角形を作る
// import java.io.*; // import java.nio.file.*; // import org.apache.poi.ss.usermodel.*; // import org.apache.poi.xssf.usermodel.*; // import org.openxmlformats.schemas.drawingml.x2006.main.*; Path path = Paths.get("shape.xlsx"); try (OutputStream os = Files.newOutputStream(path); XSSFWorkbook book = new XSSFWorkbook()) { XSSFSheet sheet = book.createSheet(); XSSFDrawing patriarch = sheet.createDrawingPatriarch(); // 図形1: 線1 XSSFSimpleShape line1 = patriarch.createSimpleShape(patriarch.createAnchor(100, 100, 0, 0, 0, 0, 3, 10)); line1.setShapeType(ShapeTypes.LINE); line1.setLineWidth(2.05); // 線の幅 2.05pt line1.setLineStyle(3); // 長破線 line1.setLineStyleColor(255, 0, 0); // 線の色 赤 // 終点 鋭い矢印 サイズ7(幅3,長さ1) CTShapeProperties line1prop = line1.getCTShape().getSpPr(); CTLineProperties line1lp = line1prop.getLn(); CTLineEndProperties line1lep = line1lp.addNewTailEnd(); line1lep.setType(STLineEndType.Enum.forString("arrow")); line1lep.setW(STLineEndWidth.Enum.forInt(3)); line1lep.setLen(STLineEndLength.Enum.forInt(1)); // 図形2: 四角形1 XSSFSimpleShape rect1 = patriarch.createSimpleShape(patriarch.createAnchor(100, 100, 0, 0, 4, 0, 8, 10)); rect1.setShapeType(ShapeTypes.RECT); rect1.setFillColor(0, 0, 255); // 塗りつぶし 青 rect1.setLineWidth(5.42); // 線の幅 5.42pt rect1.setLineStyleColor(0, 0, 0); // 線の色 黒(デフォルトでない) // 影 ⇒やり方分からなかった... book.write(os); } catch (IOException e) { throw new UncheckedIOException(e); }
影の付け方は分かりませんでした。
それ以外は、結果はHSSFと完全ではないですがだいたい同じになります。
XSSFのリバースエンジニアリングは、7-Zipアーカイバ―などでXLSXファイルを開いて、その中のxl\drawings\drawing1.xml
辺りを見てみると何か分かるかもしれません。ということでプログラムは作っていません。