Open XML SDK を使って Excel ファイルを操作する (2) - 基本構造編

クロスワープの大鷲です。

前回に引き続き、Open XML SDK(OOXML SDK)を使って、Office Open XML(OOXML)ファイルを操作する方法について紹介していきます。
今回は、Excel ファイルを構成する言語である SpreadsheetML の基本構造についてです。

Excel ファイルと SpreadsheetML

Excel ファイルの構造については、今更言うまでもなく、皆さんご存知だと思います。
ファイルは「ブック」と呼ばれ、ブックの中には複数の「シート」があります。シートは 2 次元に並んだ「セル」の集合から成ります。
そのため、OOXML SDK を使って XML ファイルを組み立てる際にも、基本的にはこのようにします。

  1. ブックを作る
  2. シートを作る
  3. 行を作る
  4. セルを作る

Excel ファイルの中身を見てみる

Excel で空のファイルを作り、拡張子を zip に変更して展開してみましょう。
中身はこんな感じになっているはずです。

Book1.xlsx
 ├─ [Content_Types].xml
 ├─ _rels
 │   └─ .rels
 ├─ docProps
 │   ├─ app.xml
 │   └─ core.xml
 └─ xl
    ├─ _rels
    │   └─ workbook.xml.rel
    ├─ theme
    │   └─ theme1.xml
    ├─ worksheets
    │   └─ sheet1.xml
    ├─ styles.xml
    └─ workbook.xml

この構造を頭の片隅に置いておいてください。

空っぽの Excel ファイルを作るコード

using (var doc = SpreadsheetDocument.Create("Book.xlsx", SpreadsheetDocumentType.Workbook))
{
	/* [1] */
	var workbookPart = doc.AddWorkbookPart();

	/* [2] */
	var workbook = workbookPart.Workbook = new Workbook();

	/* [3] */
	var worksheetPart = workbookPart.AddNewPart<WorksheetPart>();

	/* [4] */
	worksheetPart.Worksheet = new Worksheet();

	/* [5] */
	var sheets = new Sheets();
	workbook.Append(sheets);

	/* [6] */
	var sheet = new Sheet { Name = "Sheet1" };
	sheets.Append(sheet);

	/* [7] */
	string relationshipId = workbookPart.GetIdOfPart(worksheetPart);
	sheet.Id = relationshipId;

	doc.Save();
}

Excel ファイルそのものを表すクラスは SpreadsheetDocument です。

入れ物としてのファイルだけを作る

上記のコードには各行に行番号が振ってあります。
まず、これらのコードを全て無効にしてみましょう。

実質的にこういうことです。

using (var doc = SpreadsheetDocument.Create("Book.xlsx", SpreadsheetDocumentType.Workbook))
{
    doc.Save();
}

このコードを実行すると、Book.xlsx ファイルが作られます。まだ Excel で開いてはいけません。
拡張子を zip に変えて開いて見ましょう。
どうでしたか。中身は何もありませんね。

このコードは、空の zip ファイルを作るコードなのです。

少しずつ中身を組み立ててみる

では、上記のコードの [1] だけを有効にして実行してみましょう。
つまり、こういう状態です。

using (var doc = SpreadsheetDocument.Create("Book.xlsx", SpreadsheetDocumentType.Workbook))
{
    /* [1] */ var workbookPart = doc.AddWorkbookPart();
    
    doc.Save();
}

拡張子を zip に変えて開くと、こういう状態になっています。

Book1.xlsx
 ├─ [Content_Types].xml
 ├─ _rels
 │   └─ .rels
 └─ xl
    └─ workbook.xml

workbook.xml は存在しますが、中身は空っぽです。

[2]、[3] と、一段階ずつ進めていくと、こうなります。

番号 起きること
すべて無効 xlsx ファイルが作られるが、中身は空っぽ
[1] のみ有効 xl/workbook.xml が作られるが、中身は空っぽ
[2] まで有効 xl/workbook.xml に workbook 要素が作られる
[3] まで有効 xl/worksheets/sheet.xml が作られるが、中身は空っぽ
[4] まで有効 sheet.xml に worksheet 要素が作られる
[5] まで有効 xl/workbook.xml の workbook 要素内に sheets 要素が作られる
[6] まで有効 sheets 要素内に sheet 要素が作られる
[7] まで有効 sheet 要素に id 属性が書きこまれ、sheet.xml と関連付けられる

SpreadsheetML の構成要素

まだまだ完全な Excel ファイルには遠く及びませんが、一応、基本的な構成要素は見えてきました。

  1. zip ファイル内の各ファイルを表す XxxPart クラス
  2. 各 XML ファイル内の要素を表すクラス
  3. 親の要素とこのファイルを関連付ける Relationship

最後の Relationship についてちょっと補足します。
workbook.xml に sheet 要素を作り、sheet.xml に worksheet 要素を作っても、それだけでは機能しません。
sheet 要素と sheet.xml を結びつけるために、同じ ID を持った Relashinship 要素が必要なのです。

workbook.xml(抜粋)

<workbook
  xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
  xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
  <sheets>
    <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
  </sheets>
</workbook>

workbook.xml.rels(抜粋)

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet.xml"/>
</Relationships>

workbook.xml の sheet 要素の r:id="rId1" という属性は、workbook.xml.rels 内の id="rId1" 属性を持つ Relationship 要素を参照しています。
そして、この Relationship 要素が Target 属性で sheet.xml を参照することによって、sheet 要素と sheet.xml が関連付けられるのです。

一言で言うならば SpreadsheetML は、zip ファイル内のファイル ツリーと、各 XML ファイル内の XML ツリーという二重の木構造を、Relationship によって結びつけた構造と言えると思います。

その他の主な要素

これまで言及してこなかった、よく使う要素を簡単に解説しておきます。

要素 XML ファイル 意味
sheetData sheet.xml シートのデータを格納するコンテナー要素です。この中に row 要素が入ります。
row sheet.xml 行を表す要素です。子要素として c 要素を持ちます。
c sheet.xml row 要素の中にあって、セルを表す要素です。
v sheet.xml c 要素内にあって、セルの値を表します。
mergedCell sheet.xml 結合されたセルを表す要素です。
definedName workbook.xml セル範囲に対して定義した名前を表します。
calcChain calcChain.xml 計算式が参照しているセルを表します。

現実的なアプローチ

OOXML SDK を使って SpreadsheetML を一から組み上げて行くのは(Excel の互換ソフトを開発しようというのでない限り)非現実的な選択肢でしょう。
現在開発中の MODD の経理システムでは、まず Excel を使ってテンプレートとなる xlsx ファイルを作っておいて、そのファイル(のコピー)に対して、テンプレートとなる行を挿入して、データを流し込んでいくことで、帳票を作成しています。
行を挿入するということは、行番号がずれるということです。そこだけ丁寧に対応すれば、それなりに使える帳票出力コードができると思います。