はじめに

昨今、システムでExcelファイルの読み書きを行う場合、ClosedXMLなどのOSSのライブラリが使用されることが多くなっています。もともと、ExcelにはVBA相当の機能をCOMとして公開する、Microsoft.Office.Interop.Excel.dllが存在しています。従って、Excelがインストールされていれば、外部のプログラムから読み書き可能です。しかし、サーバサイドで動作させる場合

  • サーバにExcelのインストールが必要
  • 利用するクライアント数分のライセンスが必要
  • サーバサイドでExcelのUIを表示せずに利用することは、Microsoftが非推奨
    (メッセージボックスなどインタラクティブな機能が動作する可能性があるため)

等の理由から、OSSのライブラリを利用することが一般的です。

一方で、Excelをインストール済みのクライアントで利用するアプリケーションの場合、上記の制限はありません。

  • ユーザがPCを操作している状態で、起動中のExcelをそのまま制御したい
  • Excel VBAの資産を活用したい
  • 旧形式の(.xls)のファイルをそのまま使いたい

等の理由から、敢えてOSSを使用するよりも、Excel自体を制御する方が良い場合もあります。また、アプリケーションからExcelファイルの読み書きが必要な場合、その作業をするクライアントにはExcelがインストール済みで、ライセンス上の問題は発生しない場合が殆どでしょう。

今回VBAマクロの小さな社内システムをC#に置き換える機会があったので、単純なサンプルプログラムを使って、前述のDLL利用方法を紹介します。

開発環境
  • Visual Studio 2022 Professional
  • Office 365(Excelさえあれば良い)
依存関係の設定

まず、プロジェクトの依存関係のコンテキストメニューから、「COM参照の追加」を選んで、参照マネージャを起動します。ここで、”Microsoft Excel 16.0 Object Library”を探して、チェックを付けたら、OKをクリックして参照マネージャを閉じます。これで、レジストリに登録された、Microsoft.Office.Interop.Excel.dllへの参照が追加されます。

サンプルコード

Excelを起動してセルに値を入力するサンプルを示します
処理内容は以下の通りです。

  1. Excelを起動する
  2. Workbookオブジェクト、Worksheetオブジェクト、Rangeオブジェクトの順で対象の編集範囲のオブジェクトを取得する
  3. 今日から10日分の日付を配列として生成する
  4. 生成した日付を、A1からA10のセルに入力する
  5. COMオブジェクトを解放する
using System.Runtime.InteropServices;
using Excel = Microsoft.Office.Interop.Excel;
var excelApp = new Excel.Application();                 // Excel起動
var wbs = excelApp?.Workbooks;
var wb = wbs.Add();                                     // Workbook取得
var wss = wb?.Sheets;
var ws = wss[1] as Excel.Worksheet;                     // Worksheet取得
var targetRange = ws?.Range["A1:A10"] as Excel.Range;   // 編集範囲のRangeオブジェクトを取得
var columns = targetRange?.Columns as Excel.Range;
if (excelApp is not null && wb is not null && ws is not null && targetRange is not null)
{
    try
    {
        excelApp.Visible = true;                        // Excelを可視化(見せない方が実は速い)
        var startDate = DateTime.Today;                 // 今日から10日分の日付データ作成
        var dates = new DateTime[10];
        for (int i = 0; i < 10; i++)
        {
            dates[i] = startDate.AddDays(i);
        }
        targetRange.NumberFormat = "yyyy年mm月dd日";    // 編集範囲の書式指定
        targetRange.Value = dates;                      // 編集範囲にまとめて入力
        columns.AutoFit();                              // 列幅を自動調整
    }
    finally
    {
        // プラットフォーム固有のメソッド(Marshal.ReleaseComObject)を使うと警告が出るので抑止
#pragma warning disable CA1416
        Marshal.ReleaseComObject(columns);             // COMオブジェクトの解放
        Marshal.ReleaseComObject(targetRange);
        Marshal.ReleaseComObject(ws);
        Marshal.ReleaseComObject(wss);
        Marshal.ReleaseComObject(wb);
        Marshal.ReleaseComObject(wbs);
        Marshal.ReleaseComObject(excelApp);
#pragma warning restore CA1416
    }
}
注意
  • NuGetのMicrosoft.Office.Interop.Excelは使用しない
    ネットでMicrosoft.Office.Interop.Excel.dllの使い方を調べると、NuGetでインストールする方法が紹介されていることがあります。しかしNuGetには古いVersionのものしかなく、.NET 8+Office 365では利用出来ません。
    • Excelと同時にインストールされたものを利用すればOK!
  • 参照マネージャのリストから選択する
    Officeをインストールしたフォルダのdllを直接参照設定することも可能ですが、この場合はアプリ内に絶対Pathの定義が残ります。インストール時にOfficeのインストール先が変更されていない環境では動作しますが、個別の環境の絶対Pathをアプリ内に残すのはおすすめしません。
  • COMオブジェクトの解放を忘れずに!
    COMオブジェクトの解放漏れがあると、タスクマネージャにExcelのプロセスが残ってしまうことがあります。取得した回数分正しく解放すれば問題ありません。プロセス終了時にしばらく残ったままのことがありますが、ある程度時間が経過すると解放されます。なお、サンプルソースではtargetRangeにセル範囲を指定してまとめて処理しています。これは、COMオブジェクトの取得回数を減らすことで、処理の高速化と、解放漏れを防ぐ目的があります。
補足

一つ一つのCellを指定して次のように記述しても文法上は問題ありません。

ws.Cells[row,col] = DateTime.Today;

しかし、以下のコードとほぼ等価でCell単位でCOMオブジェクトを取得するため、解放漏れにつながりやすく、処理速度的にも不利なため、おすすめできません。

var cell = ws.Cells[row,col];
cell = DateTime.Today;

最後に

簡単なサンプルプログラムで、Excelファイルを操作するのではなく、Excelそのものを制御することができました。クライアント側で動作するアプリケーションでは、OSSのライブラリではできないような仕組みを実現できる可能性があります。候補の一つとして考えてみてはいかがでしょう?