Excel帳票の自動生成、つらくないですか?
Excel帳票をC#で生成していて、こんな経験ありませんか?
- Interop.Excelの COM参照 でExcelが起動するまで数秒待たされる
- COM解放を1つ忘れただけで、タスクマネージャーに
EXCEL.EXEが残り続ける -
Marshal.ReleaseComObjectを逆順で呼ばないとリークする地獄
私もずっとInteropで戦っていた。Excelで設定情報を持っていて、ローカルキャッシュに格納し、更新日時やファイルサイズの変更を検知して反映させる——という仕組みを作ったことがある。動くには動くけど、COMまわりのコードが本体より長くなって、正直「何のためのツールだっけ?」という状態だった。
ClosedXMLなら、これらの問題がまるごとなくなる。
以前書いた Excel VBAの限界と脱却ロードマップ で、VBAから脱却する方向性を紹介した。今回はその実践編として、C#でExcel帳票を生成する具体的な方法を解説する。
なぜClosedXMLか — ライブラリ比較
C#からExcelを扱うライブラリは主に3つ。選定の参考になるよう比較表にまとめた。
| 観点 | Interop | ClosedXML | EPPlus |
|---|---|---|---|
| Excelインストール | 必要 | 不要 | 不要 |
| COM解放 | 必要(忘れるとプロセスリーク) | 不要 | 不要 |
| 生成速度 | 遅い(Excel起動が必要) | 速い | 速い |
| ライセンス | Excelライセンス必要 | MIT(完全無料) | 商用有料(v5+) |
| .NET対応 | .NET Framework中心 | .NET Standard 2.0+ | .NET 6+ |
| API設計 | COM相互運用(煩雑) | 直感的なFluent API | 機能豊富だが複雑 |
| xls対応 | ○ | × (.xlsxのみ) | × |
私がClosedXMLを選んだ理由はシンプルで、MITライセンスだから商用利用も安心、そしてAPI設計が直感的で学習コストが低い。
EPPlusも機能面では優秀だけど、v5以降は商用ライセンスが有料になった。中小製造業だと「ライブラリに年額○万円の予算を取る」こと自体がハードルになる。稟議の手間を考えると、MITで使えるClosedXMLのほうが圧倒的に導入しやすい。
ただし、xls形式(古い.xls)が必要なレガシー環境では、Interopを使うしかない場面もある。新規開発なら .xlsx一択 で問題ないだろう。
環境準備 — 3分で始められる
NuGetから1コマンドでインストールできる。
dotnet add package ClosedXML
まずは動くことを確認する最小コード。
using ClosedXML.Excel;
using var workbook = new XLWorkbook();
var ws = workbook.AddWorksheet("Sheet1");
ws.Cell("A1").Value = "Hello, ClosedXML!";
workbook.SaveAs("test.xlsx");
Console.WriteLine("生成完了!");
注目してほしいのは、たった 6行 でExcelファイルが生成できること。using var で自動Dispose。COM解放のコードは0行。Excelのインストールも不要。
実践 — 試験成績書を自動生成する
ここからが本題。製造業で実際に使う 試験成績書(検査成績書)をClosedXMLで自動生成する。
完成形のイメージはこう: ヘッダーに文書情報、中段に試験情報、メインに結果テーブル、最下部に総合判定。よくある定型帳票のフォーマットだ。
以下のコードは コピペしてそのまま動く ように書いた。
using ClosedXML.Excel;
// --- データ定義 ---
var testItems = new[]
{
new { Name = "外径寸法", Spec = "50.0 ± 0.5 mm", Lower = 49.5, Upper = 50.5, Value = 50.12 },
new { Name = "内径寸法", Spec = "30.0 ± 0.3 mm", Lower = 29.7, Upper = 30.3, Value = 30.05 },
new { Name = "重量", Spec = "120 ± 5 g", Lower = 115.0, Upper = 125.0, Value = 118.7 },
new { Name = "硬度(HRC)", Spec = "58 ~ 62", Lower = 58.0, Upper = 62.0, Value = 60.3 },
new { Name = "引張強度", Spec = "≥ 800 MPa", Lower = 800.0, Upper = 9999.0, Value = 856.0 },
new { Name = "外観検査", Spec = "キズ・打痕なし", Lower = 1.0, Upper = 1.0, Value = 1.0 },
};
using var workbook = new XLWorkbook();
var ws = workbook.AddWorksheet("試験成績書");
// --- ヘッダー部 ---
ws.Cell("A1").Value = "試験成績書";
ws.Cell("A1").Style.Font.FontSize = 18;
ws.Cell("A1").Style.Font.Bold = true;
ws.Range("A1:F1").Merge().Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
ws.Cell("A3").Value = "文書番号:"; ws.Cell("B3").Value = "QC-2025-0042";
ws.Cell("A4").Value = "発行日:"; ws.Cell("B4").Value = DateTime.Today;
ws.Cell("B4").Style.DateFormat.Format = "yyyy/MM/dd";
ws.Cell("D3").Value = "作成者:"; ws.Cell("E3").Value = "山田 太郎";
// --- 試験情報 ---
ws.Cell("A6").Value = "製品名:"; ws.Cell("B6").Value = "精密シャフト PSA-200";
ws.Cell("A7").Value = "ロット番号:"; ws.Cell("B7").Value = "LOT-2025-0618";
ws.Cell("D6").Value = "試験日:"; ws.Cell("E6").Value = DateTime.Today;
ws.Cell("E6").Style.DateFormat.Format = "yyyy/MM/dd";
ws.Cell("D7").Value = "試験者:"; ws.Cell("E7").Value = "佐藤 花子";
// --- 結果テーブル ヘッダー ---
var headerRow = 9;
string[] headers = { "No.", "試験項目", "規格値", "測定値", "判定" };
for (int i = 0; i < headers.Length; i++)
{
var cell = ws.Cell(headerRow, i + 1);
cell.Value = headers[i];
cell.Style.Font.Bold = true;
cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#4472C4");
cell.Style.Font.FontColor = XLColor.White;
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
}
// --- 結果テーブル データ ---
bool allPassed = true;
for (int i = 0; i < testItems.Length; i++)
{
var item = testItems[i];
int row = headerRow + 1 + i;
bool passed = item.Value >= item.Lower && item.Value <= item.Upper;
if (!passed) allPassed = false;
ws.Cell(row, 1).Value = i + 1;
ws.Cell(row, 2).Value = item.Name;
ws.Cell(row, 3).Value = item.Spec;
ws.Cell(row, 4).Value = item.Name == "外観検査" ? "合格" : item.Value;
ws.Cell(row, 5).Value = passed ? "合格" : "不合格";
ws.Cell(row, 5).Style.Font.FontColor = passed ? XLColor.FromHtml("#008000") : XLColor.Red;
ws.Cell(row, 5).Style.Font.Bold = true;
}
// --- 総合判定 ---
int summaryRow = headerRow + testItems.Length + 2;
ws.Cell(summaryRow, 1).Value = "総合判定:";
ws.Cell(summaryRow, 1).Style.Font.Bold = true;
ws.Cell(summaryRow, 2).Value = allPassed ? "合格" : "不合格";
ws.Cell(summaryRow, 2).Style.Font.FontSize = 14;
ws.Cell(summaryRow, 2).Style.Font.Bold = true;
ws.Cell(summaryRow, 2).Style.Font.FontColor = allPassed ? XLColor.FromHtml("#008000") : XLColor.Red;
// --- 書式設定 ---
var tableRange = ws.Range(headerRow, 1, headerRow + testItems.Length, 5);
tableRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
tableRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
ws.Column(1).Width = 6;
ws.Column(2).Width = 16;
ws.Column(3).Width = 20;
ws.Column(4).Width = 14;
ws.Column(5).Width = 10;
// --- 保存 ---
workbook.SaveAs("試験成績書_2025.xlsx");
Console.WriteLine($"生成完了: 総合判定 = {(allPassed ? "合格" : "不合格")}");
少しコードが長く見えるかもしれないけど、やっていることはシンプルだ。
- データ定義: 匿名型の配列で試験項目・規格値・測定値を持つ
-
ヘッダー部:
Merge()でセル結合し、文書番号・発行日を配置 - 結果テーブル: ループで各項目を書き込み、上限・下限と比較して合否判定
- 書式設定: 罫線・背景色・列幅を一括設定
ハマったのは、外観検査のような「数値じゃない項目」の扱い。数値比較のロジックに乗せるため 1.0 を合格値として入れて、表示だけ「合格」にしている。こういう小さな工夫が現場コードには必要になる。
このコードを実行すると、罫線つき・色分けされた試験成績書のExcelファイルが約 0.3秒 で生成される。Interopだと同じ内容でも5〜10秒はかかっていた。
Before/After — Interop vs ClosedXML
同じ「セルに値を入れてBold+保存」をやるだけで、こんなに違う。
Interop(COM解放の連鎖に注意):
// Interop — COM解放の連鎖に注意
var app = new Microsoft.Office.Interop.Excel.Application();
var workbooks = app.Workbooks;
var workbook = workbooks.Add();
var sheets = workbook.Sheets;
var sheet = (Worksheet)sheets[1];
var range = sheet.Range["A1"];
range.Value = "試験成績書";
range.Font.Bold = true;
// 解放の順番を間違えるとExcel.exeが残る...
Marshal.ReleaseComObject(range);
Marshal.ReleaseComObject(sheet);
Marshal.ReleaseComObject(sheets);
workbook.SaveAs(@"C:\output\report.xlsx");
workbook.Close();
Marshal.ReleaseComObject(workbook);
Marshal.ReleaseComObject(workbooks);
app.Quit();
Marshal.ReleaseComObject(app);
ClosedXML(COM解放? なにそれ):
// ClosedXML — COM解放? なにそれ
using var workbook = new XLWorkbook();
var ws = workbook.AddWorksheet("試験成績書");
ws.Cell("A1").Value = "試験成績書";
ws.Cell("A1").Style.Font.Bold = true;
workbook.SaveAs(@"report.xlsx");
// おしまい。
Interopは 10行以上のCOM解放コード が必要。ClosedXMLは 5行で完結。
正直に言うと、このBefore/Afterを並べて見た瞬間、「なんで今までInterop使ってたんだろう」と思った。COMの解放順を調べたり、ReleaseComObject のラッパーを書いたりした時間はなんだったのか。
ハマりポイントと対策
ClosedXMLは使いやすいけど、最初に知っておくと助かるポイントが4つある。
1. 日付がシリアル値で表示される
ws.Cell("A1").Value = DateTime.Today;
// ↑ これだけだと「45827」みたいな数値で表示される
ws.Cell("A1").Style.DateFormat.Format = "yyyy/MM/dd";
// ↑ 書式を指定して初めて「2025/06/18」になる
最初「なんで日付が数字になるんだ?」と焦った。Excelの内部表現がシリアル値だから当然なのだけど、Interopでは自動で書式が当たっていたので気づかなかった。
2. 数値は数値型のまま入れる
ws.Cell().Value = "123" と文字列で入れると、Excel上で数値として扱えなくなる。SUM 関数が効かなくなって困ることになるので、int/doubleはそのまま代入 するのがおすすめ。
3. テンプレートの読み込みも簡単
using var workbook = new XLWorkbook("template.xlsx");
var ws = workbook.Worksheet("Sheet1");
ws.Cell("B5").Value = "差し込みデータ";
workbook.SaveAs("output.xlsx");
既存のExcelテンプレートを開いて、値を差し込んで保存。社内で使っている帳票フォーマットをそのまま活かせる。
4. 数万行のデータでも大丈夫
Interopだと数千行の書き込みでExcelが固まることがあったけど、ClosedXMLはメモリ上で処理が完結するので、5万行のデータでも数秒で書き出せる。サーバー環境でも安定して動作する。
まとめ
-
Excel Interopからの移行は想像以上に簡単 — NuGet追加 →
using varで書くだけ - 試験成績書のような定型帳票はClosedXMLと相性がいい — テーブル + 書式設定のAPIが直感的
- MITライセンスで商用利用にも安心 — 稟議不要でプロジェクトに導入できる
よくある質問
Q: 既存の.xlsxテンプレートを使えますか?
A: new XLWorkbook("template.xlsx") でテンプレートを開いて、値を差し込んで別名保存できる。社内帳票のフォーマットをそのまま活用できるのが強み。
Q: .xls形式は読めますか?
A: ClosedXMLは.xlsx/.xlsmのみ対応。レガシーな.xlsファイルが必要な場合はNPOIを検討するといい。
Q: グラフは作れますか?
A: ClosedXMLはグラフ生成に対応していない。グラフが必要なら、EPPlusを使うか、テンプレートにグラフを事前定義しておいてデータだけ差し込む方法がある。
Q: .NET Frameworkでも使えますか?
A: .NET Standard 2.0対応なので、.NET Framework 4.6.1以降で利用可能。既存のWinFormsプロジェクトにもそのまま追加できる。
筆者: JodyCraft — 製造業×C#エンジニア。生産技術職として5年以上の実務経験を持ち、品質管理・計測器制御・業務自動化を専門とする。
GitHub: @joji804 / Zenn: @jodycraft
📝 この記事は Zenn で最初に公開されました。
最新版はZennをご覧ください。