DelphiおよびC++Builder向けのネイティブExcelライブラリであるHotXLSは、ロスレスなXLSXラウンドトリップ(往復処理)向けに設計されています。ワークブックを開き、1つのセルを変更して保存するだけで、顧客のカスタムテーマ、外部のextLst拡張ブロック、および計算チェーン(calculation chain)がすべて維持されます。これを実現するために、xl/theme/theme1.xmlのそのままのキャッシュ保存、不明な<ext>ブロックのイベントベースの再シリアル化、および数式ワークブックの保存ごとの仕様に準拠した新しいxl/calcChain.xmlの生成という3つの仕組みが機能しています
これら3つの仕組みが必要となる状況は、非常によく見られるものです。請求サービスが、顧客がExcelでデザインしたテンプレート(企業指定のカラーテーマ、KPI列のスパークライン、新しいExcelビルドで追加された条件付き書式ルールなど)をロードし、セルB3に請求書の合計額を1つ書き込み、保存するとします。顧客が出力ファイルを開くと、ブランドカラーは標準のOfficeブルーに戻り、スパークラインは消去され、Excelはファイルの「修復」を提案します。コード自体はそれらの機能に一切触れていませんが、ライブラリが単に保存処理を行っただけでこのような変化が発生してしまうのです
なぜライブラリで編集するとExcelファイルから書式が失われるのか?
ライブラリで編集した後にExcelファイルから書式が失われるのは、ほとんどのライブラリがファイルを編集しているのではなく「再構築」しているためです。.xlsxパッケージは、xl/workbook.xml、各シートごとのxl/worksheets/sheetN.xml、xl/styles.xml、xl/theme/theme1.xml、xl/calcChain.xmlなどのXMLパーツを含むZIPファイルです。一般的なライブラリは、ロード時にこれらのパーツをオブジェクトモデルにパースし、保存時にそのモデルからすべてのパーツを再生成します。オブジェクトモデルで表現されていない機能(パースされなかったテーマや、新しいExcelで追加された拡張ブロックなど)はメモリ上に保持できないため、再生成されたパーツから暗黙的に省略されてしまいます
ECMA-376はこの課題を半分予測していました。SpreadsheetMLは、extLst(ECMA-376 Part 1の「Future Feature Data Storage Area」、ワークブックレベルの要素については§18.2.10)を定義済みの拡張ポイントとして規定しています。新しい生成ツールはここに機能を配置し、各機能は識別用のuri属性を持つ<ext>要素にラップされます。古いコンシューマは、自身が解釈できないデータであってもこれを維持することが期待されます。スパークライン、スライサー、新しい条件付き書式タイプなどはすべてこの方法で格納されます。したがって、不明な<ext>ブロックを破棄するライブラリは、単に再現性が低いだけでなく、ファイル形式が前提としている前方互換性の契約に違反していることになります。スプレッドシートライブラリを評価する際の質問は単純です。「1つのセルを変更したとき、他に何が変更されるか」という点です
HotXLSがカスタムテーマをバイト単位で完全に維持する仕組み
HotXLSは、ロード時にxl/theme/theme1.xmlの元のバイトデータをキャッシュし、保存時にそれをそのまま書き戻すことでワークブックのテーマを維持します。テーマパーツ(ECMA-376 Part 1、§14.2.7)はSpreadsheetMLではなく、配色、フォント体系、書式体系などを定義するDrawingMLであり、スプレッドシートエンジンがその内部構造を詳細にモデル化する必要はありません。HotXLSの初期バージョンは保存のたびに固定のOfficeテーマを再生成していたため、上記の「ブランドカラーが標準カラーに戻る」問題が発生していました。v2.89.46以降は、ロードされたパッケージのテーマがそのまま保存されて無変更で再出力され、内蔵のOfficeテーマは新規作成されたワークブックに対してのみ生成されます。未加工のバイトデータを扱うことは、最も確実な再現性の保証となります。パースや再シリアル化を行わないため、データが変化する余地はありません
このそのままのコピー処理は、プログラムによるテーマアクセス処理よりも優先されます。TXLSXWorkbookは新規ワークブックのヘッダーや本文のフォントを選択できるようにThemeMajorFontおよびThemeMinorFontを公開していますが、ロード時に元のテーマがキャプチャされている場合、これらのセッターは保存されるファイルに影響を与えません(ラウンドトリップの再現性が優先されます)。既存のワークブックのテーマをどうしても変更する必要がある場合は、データAPIを使用するのではなく、Excel自体でテンプレートを編集すべきだというサインです。日常的なユースケースではAPIの操作は必要ありません
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('branded-invoice.xlsx');
Book.Sheets[0].Cells[3, 2].Value := 42750.00; // the one edit
Book.SaveAs('branded-invoice-out.xlsx');
// theme1.xml in the output is byte-identical to the input
finally
Book.Free;
end;
end;
保存時に不明なextLstブロックはどうなるのか?
HotXLSは、ネイティブでモデル化していないすべてのワークシートレベルの<ext>ブロックをキャプチャし、保存されたワークシートのextLstに再出力します。これにより、新しいExcelビルドで書き込まれた機能がラウンドトリップ後も正常に維持されます。v2.131.0以降、キャプチャされたフラグメントは各XLSXワークシートの読み取り専用プロパティであるRawWorksheetExts(TStringList型)を介して確認できるようになりました。これにより、再現性の保証を単なる信頼ではなくテストコードから監査可能になります
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
i: Integer;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('from-newer-excel.xlsx');
Sheet := Book.Sheets[0];
WriteLn(Format('%d foreign ext block(s) captured',
[Sheet.RawWorksheetExts.Count]));
for i := 0 to Sheet.RawWorksheetExts.Count - 1 do
WriteLn(Copy(Sheet.RawWorksheetExts[i], 1, 100)); // peek at each uri
finally
Book.Free;
end;
end;
知っておくべき実装の詳細として、このキャプチャ処理は生のバイトコピーではなく、イベントレベルの再シリアル化であるという点があります。HotXLSのストリーミングXMLリーダーはソースのオフセット位置を公開しないため、認識できないサブツリーはXMLイベント(Element、Text、EndElement)のストリームから再構築されます。このアプローチには、典型的な罠があります。<a/>のような自己終了要素は、空フラグ付きのElementイベントのみを発生させ、EndElementイベントを発生させません。そのため、EndElementのみでインクリメント・デクリメントを行う深度カウンタは、サブツリーの終了を正しく検出できません。この罠に対処した上で再構築されたフラグメントは、意味的に元のデータと同等になります。属性のクォーテーションや自己終了形式が正規化されるため、バイト単位で同一にはなりませんが、Excelはバイトデータではなく意味を読み取るため問題ありません。Excelの出力が持つ2つの性質により、この出力は安全に行われます。Excelは必要なxmlns属性を<ext>要素自体またはその内部で宣言するため、キャプチャされた各フラグメントは名前空間として自己完結しています。この自己完結性により、ワークブック内またはワークブック間でのワークシートの複製を行う際にも、単純な文字列リストの代入操作だけで外部ブロックを一緒に転送できます
Excelが数式を信頼できるようにcalcChain.xmlを書き込む
HotXLSは、保存されるワークブックに数式が含まれている場合に常にxl/calcChain.xml(計算チェーンパーツ、ECMA-376 Part 1、§12.3.1)を書き込み、2つの並び順から選択します。数式依存関係グラフがすでに構築されて最新である場合(最後の編集後にRecalculateを呼び出した場合)、チェーンは依存関係が考慮された完全なトポロジカル順で出力され、循環参照のあるメンバーは末尾に追加されます。それ以外の場合は、セルはドキュメント順にリストされます。Microsoftの仕様実装ノート[MS-XLSX]において、計算チェーンはExcelがロード時に検証および再編成するヒント情報として扱われるため、どちらの出力も正当です。HotXLSは、SaveAsの内部でグラフ構築を強制することをあえて避けています。エッジの構築コストはセル数の2乗に比例するため、数百万セルを含む保存処理において許容できない隠れたコストになるためです
Book.Open('model.xlsx');
Book.Sheets[0].Cells[10, 4].Formula := '=SUM(D2:D9)';
// Saved now, calcChain.xml lists formula cells in document order.
// After Recalculate the dependency graph exists, so the same save
// emits a full topological order instead:
Book.Recalculate;
Book.SaveAs('model-out.xlsx');
ロスレスラウンドトリップの再現性の限界
マーケティングのチェックボックスを満たすことよりも正確性が重要であるため、再現性の限界についても明記します。HotXLSはパッケージ全体をバイト単位で完全にコピーするわけではありません。ワークシートのXML、スタイル、共有文字列、およびワークブックパーツはパースされたモデルから再生成されるため、出力は意味論的には正確ですがバイナリとしては同一ではありません(ZIPのローカルヘッダーだけでも新しいタイムスタンプが書き込まれます)。キャプチャされた<ext>フラグメントは上述の通り正規化されて出力されます。元のテーマが存在する場合、プログラムによるテーマフォントの上書きは無視されます。また、この維持処理のネットワークには対応範囲が設定されています。すなわち、HotXLSがネイティブでモデル化している機能(例えば、スパークラインは単純なコピーではなくパースされて書き換えられます)、外部のextLstコンテンツ、およびそのままキャッシュされたパーツが対象となります。モデル化されておらず、かつ拡張ポイントにも含まれないパーツ(例えば、特殊なアドイン独自のカスタムパーツなど)は、本記事で説明した3つの仕組みの対象外となるため、動作を仮定せず実際のテンプレートでテストを行ってください
関連する他の維持機能もスプレッドシート処理を補完します。VBAプロジェクトおよび外部ワークブックへの参照は、「モデル化していないデータも維持する」という同じ方針のもとで保存処理を通過します。これについては、姉妹編の記事であるVBAおよび外部リンクの維持で説明しています。また、docProps内のドキュメントプロパティは、暗黙的に破棄されることなく、専用 of 読み書きAPIが提供されています。スプレッドシートライブラリを評価する際は、1セルの変更テストを実行してみてください。機能豊富な本番用ワークブックを開き、1つの値を変更して保存し、展開したパーツと元のファイルをdiffで比較します。変更を加えたシート以外のどの部分が変更されたかによって、機能比較表以上にそのライブラリの性質を深く理解することができます
ここで説明したラウンドトリップ機構(v2.89.46からのテーマのそのままの保持、v2.131.0からの外部extLstのキャプチャおよびcalcChain.xmlの出力)は、DelphiおよびC++Builder向けの完全なXLSX読み書き機能を備えた現在のHotXLS Delphi Excel Componentに含まれています