ほとんど何もしないジョブを考えてみます。月次のワークブックを開き、1つのセルに今日の日付を書き込み、保存し直すだけです。これをサービスとして何度も走らせていると、それでも苦情が届きます。マクロが消えている、あるいはリンクしていた為替レートが#REF!になっている、そして運用チームはあなたのコードがそれらを削除したと確信しています。実際には何も削除していません。たいていの場合に起きているのは、マクロ有効のワークブックが素の.xlsxという名前で出力され、ExcelがECMA-376のコンテンツタイプの規則に従っただけのことです。コンテンツタイプがVBAを宣言していないパッケージは、たとえそのバイト列がすぐそこにあってもVBAプロジェクトを読み込めません。ファイルが壊れたわけではありません。Excelが一部を無視せざるを得ない状態へと改名されたのです
マクロと外部ワークブックリンクは、自動化が最も確実に失う2つの要素であり、その根っこの理由は同じです。どちらも編集コードが実際に触れるセルのグリッドの外側に存在するため、行と列で物事を考えるコードは、削除命令を一度も出さないままそれらを落としてしまいます。HotXLSはExcelを導入せずにXLSとXLSXを読み書きする、DelphiとC++Builder向けのネイティブライブラリであり、この2つを「たまたまコピーされるデータ」ではなく「意図して運ぶ積み荷」として扱います。以下では、それぞれが保存経路に何を要求し、保証がどこで途切れるのかを見ていきます
書き換えのもとでこの2つが異なる振る舞いをする理由
VBAプロジェクトは1つの不透明なバイナリです。OOXMLパッケージではvbaProject.binというファイルであり、旧来のBIFFファイルではOLEストレージです。これを失う道はちょうど2つあります。ライターがそれを出力へコピーしないか、出力がそれを許さないファイル種別になるかです。どちらの失敗も全面的で、しかも無言です。プロジェクトはあるか、ないかのどちらかです
一方、外部リンクはそもそもブロブではありません。小さな関係のグラフです。別のワークブックを指すパスまたはURL、その参照先が公開しているシート名の一覧、そして参照先がオフラインでもExcelが何かを表示できるように、それらのシートで最後に見えた値のキャッシュ(任意)から成ります。この3つの部分は書き換えのもとで寿命が異なり、ライブラリによっては一部を忠実に保持しつつ、残りを黙って落とすことがあります。この非対称性こそ正確に把握しておく価値のある部分です。セル編集のコードからは何も見えてこないからです
XLSXの書き換えを通してVBAプロジェクトを運ぶ
XLSX側では、TXLSXWorkbookがマクロの積み荷をそのまま保持します。VbaProjectプロパティが生のvbaProject.binのバイト列をAnsiStringの中に保持し、空文字列はマクロが存在しないことを表します。その周りには3つの操作があります。HasVbaProjectはプロジェクトの有無を答え、ClearVbaProjectは意図的に取り除き、LoadVbaProjectFromFileはテンプレートから抽出したものを注入します。最後の呼び出しは見た目以上の価値があります。テンプレートファイル全体をパイプラインに引きずり込まずに、生成したワークブックへ標準のマクロプロジェクトを取り込めるからです
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Data');
Sheet.Cells[1, 1].Value := 'Refreshed ' + DateTimeToStr(Now);
Book.LoadVbaProjectFromFile('macros\vbaProject.bin');
if not Book.HasVbaProject then
raise Exception.Create('VBA payload failed to load');
// .xlsm 拡張子は見た目の問題ではない。パッケージ内の
// マクロ有効コンテンツタイプを選択するのはこの拡張子である。
Book.SaveAs('monthly-report.xlsm');
finally
Book.Free;
end;
end;
問題全体の分かれ目は保存の行にあります。VBAプロジェクトを保持するワークブックはマクロ有効のセマンティクスで書き出す必要があり、HotXLSは出力名が.xlsmで終わるときにそれを適用します。代わりに.xlsxを渡すと、バイト列が物理的にパッケージの中にあり、問題なくデシリアライズできるはずでも、Excelはマクロを拒否します。拡張子は飾りではありません。VBAプロジェクトの存在が許されることをExcelに伝えるコンテンツタイプを選ぶのが拡張子です。多くの場合、必要なのは積み荷を通過させることだけです。監査レポート用にモジュール名を列挙するなど、中身を読み取る必要がある場合は、ParsedVBAProjectが解析済みのモジュールモデルを公開し、VbaProjectのほうは手つかずの元のバイト列のままです
旧来のXLSワークブックからマクロを再利用する
BIFFのファサードは、ひと手間だけ多い形で同じ道具立てを備えています。HasVBAProjectは読み込んだファイルを調べ、SaveVBAProjectToFileはプロジェクトのストレージをディスクへ書き出し、LoadVBAProjectFromFileはそれを別のワークブックへ読み戻します。いったんファイルを経由することで、よくある近代化の作業が素直に片づきます。2003年頃のモデルからマクロを取り出し、新しく生成したXLSの出力へ植え込むのに、実行時に元のテンプレートを用意する必要はありません
var
Src, Dst: IXLSWorkbook; // インターフェイス参照: 手動のFreeは不要
begin
Src := TXLSWorkbook.Create;
if Src.Open('legacy-model.xls') <= 0 then
raise Exception.Create('Cannot open legacy model');
if Src.HasVBAProject then
Src.SaveVBAProjectToFile('extracted-vba.bin');
Dst := TXLSWorkbook.Create;
Dst.Sheets.Add.Name := 'Report2026';
Dst.LoadVBAProjectFromFile('extracted-vba.bin');
Dst.SaveAs('report-with-macros.xls');
end;
ここでの落とし穴はメモリモデルであり、XLSXのクラスとは逆になっています。TXLSWorkbookは参照カウント方式のIXLSWorkbookインターフェイス越しに保持するため、手で解放してはいけません。XLSX側のTXLSXWorkbookは素のオブジェクトなので、try..finallyで包んで解放する必要があります。1つのユニットで2つの流儀を混ぜると、二重解放によるクラッシュが待っています。もう1つ守るべき境界があります。抽出と注入は同じファイル形式の内側で完結させることです。BIFFのプロジェクトストレージとOOXMLのvbaProject.binはいとこ同士であって同じコンテナではないので、両方の形式でマクロを出力するパイプラインは、形式ごとに別のマクロテンプレートを持つべきです
外部リンク:地図は残るが、キャッシュ値は残らない
XLSXのワークブックでは、HotXLSはExternalLinksコレクションを通じて外部リンクを公開します。各TXLSXExternalLinkは、リモートのワークブックのパスまたはURLであるTargetと、参照しているシートを列挙するSheetNamesリストを保持します。どちらも開いて保存する往復で無傷のまま残り、リンクをゼロから組み立てることもできます:
var
Link: TXLSXExternalLink;
begin
Link := Book.ExternalLinks.Add('\\fileserver\finance\fx-rates-2026.xlsx');
Link.SheetNames.Add('FX');
if Book.ExternalLinks.Count > 0 then
Writeln(Format('%d external link(s): delivery requires reachable targets',
[Book.ExternalLinks.Count]));
end;
境界は参照先の一覧よりもう1段深いところにあります。HotXLSはリンクの地図、つまり参照先とシート名を往復させますが、OOXMLがリンクのsheetDataSet要素に保持しているキャッシュ済みのセル値は解析も書き換えもしません。このキャッシュは、元ファイルがオフラインのときにExcelが最後に判明していた数値を表示するためのものであり、生成されたワークブックにはそれが入りません。その影響はあなたではなく受け取り手に降りかかります。参照先に到達できない環境、たとえばVPNから外れたノートPCや、名前が変わった共有フォルダーでそうしたファイルを開くと、リンクに依存する数式は#REF!になるか、更新の確認ダイアログの後ろで止まります。ここから2つの規則が導かれます。生成したワークブックが外部リンクの値をオフラインで表示できると約束しないこと。そしてExternalLinks.Countがゼロでないことは、機能ではなく配布の前提条件として読むことです。ファイルが実際に開かれる場所から、すべての参照先に到達できなければなりません
XLSリーダーがバイト単位で保持するもの
モデル化していない構造について、BIFF側は別の答えを用意しています。見つけたままの姿で残すことです。ピボットキャッシュとピボットビュー(SX*レコード群)、QueryTableの定義、外部データ接続、ユーザー設定のビュー、ヘッダー画像、テーマのレコードは、いずれも開いて保存する往復を、解析も変更もされない生のレコードブロックとして通過します。外部参照そのものは、下層のEXTERNSHEETおよびSupBookレコードを介して往復します。XLS側にはこれらを型付きで新規作成するAPIはありませんが、既存のリンクは編集を経ても手つかずで残ります
バイト単位の保持は本物の保証ですが、鋭い角があります。保持された構造を誰も読まないからこそ、あなたの編集がそれを壊すこともありません。同じ理由で、誰もそれを更新しません。保持されたピボットキャッシュやクエリテーブルが指している領域に行を挿入すると、下のデータがずれる一方で構造は元の座標を持ち続けます。ファイルは依然として妥当なXMLまたはBIFFですが、意味は静かにずれてしまい、それを知らせるエラーは何も出ません。守りやすい設計は、生成による編集を、保持対象の構造を持たないシートに限ることです。これはワークシート保護とページ設定に関する記事で、ロックされたシートや印刷設定済みのシートを守る作法と同じです
実際に書き出したファイルを検証する
どちらの失敗も書き込み時には無言なので、意味のある確認は、生成したコードを信用することではなく、出力を開き直して行うものになります。3つの検査でほぼすべてを覆えます。ファイルを開き直し、マクロがあるはずの場合にHasVbaProjectが依然としてtrueを返すことを確認します。これは積み荷の欠落と拡張子の誤りを1つのテストで捕まえます。次にExternalLinks.Countを読み、書き換え前の件数と突き合わせます。そのうえで、マクロを無効にした状態でExcelで1回開いてみます。Excelのコンテンツタイプ検証はどのライブラリよりも厳しく、しかも顧客がファイルを判断するのはExcelというプログラムだからです
これらのどれも、入口で完全な解析を必要としません。ワークブックが大量に届き、統制対象のコンテンツを持つものを仕分けたいだけなら、シート一覧と軽量なワークブック検査に関する記事で紹介している軽い調査によって、最初の書き換えが走る前に、マクロを持つファイルやリンクを持つファイルをより厳格なパイプラインへ振り分けられます
よく寄せられる質問がいくつかあるので、ここで直接答えておきます。HotXLSは保持しているマクロを実行することはありません。ライブラリの中にVBAランタイムはなく、プロジェクトをデータとして保存、複製、抽出、注入するための仕組みしかありません。サーバー上ではこれは明言する価値のあるセキュリティ上の性質です。悪意あるマクロがパイプラインを通過しても、デスクトップのExcelがファイルを開き、利用者がコンテンツを有効にするまでは不活性なままだからです。.xlsmを.xlsxへ変換してマクロを残すことはできませんが、これはライブラリの制約ではなく形式そのものの規則です。.xlsxのコンテンツタイプはマクロを含まないワークブックを宣言するため、誠実な結末は.xlsmのままにするか、ClearVbaProjectを呼んで本当にマクロを持たないファイルを出すかのどちらかです。黙って改名するという選択だけが、誰も満足させません。そして書き換えの後にリンク先のセルが#REF!を表示する場合、原因は上で述べた値キャッシュの欠落です。新しいファイルは参照先を運びますがキャッシュされた数値は運ばないため、Excelは開いた時点で元ファイルを解決しなければならず、到達できないパスや環境依存の相対パスではそれが果たせません。参照先に到達できることを保証するか、配布前に計算済みの値をセルへ書き込んで依存関係そのものを取り除いてください
他人のワークブックを編集する作業は、その大半が、自分が書いたものでもなく完全には理解していないものを保持する作業です。ここで述べたVBAと外部リンクのラウンドトリップ機能は、DelphiおよびC++Builder向けのHotXLS Delphi Componentに、ファイルが届いた瞬間に統制対象のコンテンツを検出できる監査用プロパティとともに同梱されています