1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C# ClosedXMLでExcel帳票を自動生成する — Interopの苦痛から解放される実践ガイド

1
Last updated at Posted at 2026-04-01

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 ? "合格" : "不合格")}");

少しコードが長く見えるかもしれないけど、やっていることはシンプルだ。

  1. データ定義: 匿名型の配列で試験項目・規格値・測定値を持つ
  2. ヘッダー部: Merge() でセル結合し、文書番号・発行日を配置
  3. 結果テーブル: ループで各項目を書き込み、上限・下限と比較して合否判定
  4. 書式設定: 罫線・背景色・列幅を一括設定

ハマったのは、外観検査のような「数値じゃない項目」の扱い。数値比較のロジックに乗せるため 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プロジェクトにもそのまま追加できる。

この記事の内容を貴社の業務に適用しませんか?

製造業の生産技術職として5年以上、C#で業務ツールを開発してきました。
Excel帳票の自動化、VBAからの移行、試験データ管理システムの構築など、
「現場でそのまま使える」ツール開発を得意としています。

  • 現状のExcel/VBA業務の課題をヒアリング
  • 移行の方向性と優先順位をフィードバック
  • 30分程度のオンライン相談(無料)

お問い合わせはこちら(ポートフォリオサイト)


筆者: JodyCraft — 製造業×C#エンジニア。生産技術職として5年以上の実務経験を持ち、品質管理・計測器制御・業務自動化を専門とする。
GitHub: @joji804 / Zenn: @jodycraft


📝 この記事は Zenn で最初に公開されました。
最新版はZennをご覧ください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?