インストールされている Excel のバージョンに依存せずに .NET で Excel の処理を行う

Siden oppdatert :
ページ作成日 :

動作確認環境

Visual Studio
  • Visual Studio 2022
.NET
  • .NET 6.0
Windows
  • Windows 7
  • Windows 11
Excel
  • Microsoft 365
  • Office 2007

動作必須環境

Windows
  • いずれかのバージョン
Excel
  • いずれかのバージョン

Excel を操作するライブラリの問題点

プログラムから Excel を操作する方法のひとつとして Microsoft.Office.Interop.Excel を参照する方法があります。 これは COM を経由して Excel のアプリケーションを直接操作する形に近いため、Excel ファイルのデータを処理するのとは若干使い方が異なります。 前提として実行する環境に Excel がインストールされている必要があるのですがその分 Excel の多くの機能がプログラム側から利用可能となっています。

Microsoft.Office.Interop.Excel を使用した場合プログラムは概ね以下のような書き方になると思います。

// 実行プログラムの場所にある Excel ファイル
var excelFilePath = $@"{Path.GetDirectoryName(System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName)}\Sample.xlsx";

// Excel のオブジェクトは参照したら必ず解放する必要があります。
// 解放しないと Excel のプロセスが残り続けます。
Microsoft.Office.Interop.Excel.Application? excel = null;
Microsoft.Office.Interop.Excel.Workbooks? books = null;
Microsoft.Office.Interop.Excel.Workbook? book = null;
Microsoft.Office.Interop.Excel.Sheets? sheets = null;
Microsoft.Office.Interop.Excel.Worksheet? sheet = null;
Microsoft.Office.Interop.Excel.Range? cells = null;
Microsoft.Office.Interop.Excel.Range? range1 = null;
Microsoft.Office.Interop.Excel.Range? range2 = null;
try
{
  // Excel アプリケーションを生成(起動)します
  excel = new Microsoft.Office.Interop.Excel.Application
  {
    DisplayAlerts = false
  };

  // ワークブック一覧を参照します。参照する場合は必ず変数に保持します
  books = excel.Workbooks;

  // Excel ファイルを開きます
  book = books.Open(excelFilePath);

  // シート一覧を参照するので変数に保持します
  sheets = book.Worksheets;

  // シートを参照します。最初のシートは 1 になります
  sheet = (Microsoft.Office.Interop.Excel.Worksheet)sheets[1];

  // セル一覧を参照します
  cells = sheet.Cells;

  // 左上のセルを参照します。一番左上は [1, 1] になります
  range1 = (Microsoft.Office.Interop.Excel.Range)cells[1, 1];

  // セルの値を取得します。値の取得なので後で解放する必要はありません
  var value = (int)range1.Value;

  // 下のセルを参照します
  range2 = (Microsoft.Office.Interop.Excel.Range)cells[2, 1];

  // 日付分足してセットします
  range2.Value = DateTime.Now.Day + value;

  // 保存して閉じます
  book.Close(SaveChanges: true);

  Console.WriteLine("処理が完了しました。");
}
catch (Exception ex)
{
  // 閉じていなければ保存せずに閉じます
  // 開きっぱなしだと Excel 起動時に保存されていないデータとして表示される場合があります
  if (book != null) book.Close(SaveChanges: false);

  Console.WriteLine("処理が失敗しました。");
  Console.WriteLine(ex);
}
finally
{
  // 終了していなければ終了します
  if (excel != null) excel.Quit();

  // 例外が発生した場合でも必ずリソースを解放するようにします
  if (range1 != null) System.Runtime.InteropServices.Marshal.FinalReleaseComObject(range1);
  if (range2 != null) System.Runtime.InteropServices.Marshal.FinalReleaseComObject(range2);
  if (cells != null) System.Runtime.InteropServices.Marshal.FinalReleaseComObject(cells);
  if (sheet != null) System.Runtime.InteropServices.Marshal.FinalReleaseComObject(sheet);
  if (sheets != null) System.Runtime.InteropServices.Marshal.FinalReleaseComObject(sheets);
  if (book != null) System.Runtime.InteropServices.Marshal.FinalReleaseComObject(book);
  if (books != null) System.Runtime.InteropServices.Marshal.FinalReleaseComObject(books);
  if (excel != null) System.Runtime.InteropServices.Marshal.FinalReleaseComObject(excel);
}

都度リソースの解放を行わなければ行けないのが面倒ですが、用意されているクラスを使えるので割と直観的に Excel の処理を行うことができます。

しかし欠点としてはプログラムで参照するライブラリのバージョンと、実行する環境でインストールされている Excel のバージョンが一致、または互換性がある必要があります。 例えば Excel の内部バージョン 15.0 (2013) のライブラリを参照して作成したプログラムは Excel 2013 がインストールされている環境でしか実行できません。 プログラムを多くの環境で実行させる場合は Excel のインストールのバージョンを統一しておく必要が出てきます。

そういう運用が可能な環境であればそれでもいいですが、インストールされている Excel のバージョンがまちまちだと対応できません。 そのためプログラム側でどうにかする必要があります。

ライブラリを直接参照せず dynamic で動的に参照する

今回問題が発生するのはプログラムを作成した時点でライブラリのバージョンを固定で参照しているためです。 という事はプログラム作成時点でバージョンを決めるのではなくプログラムの実行時にバージョンを判別すればいいと言うわけです。

方法はいくつかありますが、今回は「Type.GetTypeFromProgID」「Activator.CreateInstance」「dynamic」を使用して解決します。

通常 C# ではプログラム作成時点で型情報を決める必要がありますが、型を dynamic にすると実行時点で型を決めることが出来ます。 object 型とは違い dynamic 変数に設定されたインスタンスがクラスであればそのクラスのメソッドやプロパティを呼ぶことも出来ます。 ただ実行中のタイミングで変数に何が入るか決まるため、指定したメソッドがなければ実行時にエラーになります。

これらを利用して作成したコードが以下になります。

// 実行プログラムの場所にある Excel ファイル
var excelFilePath = $@"{Path.GetDirectoryName(System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName)}\Sample.xlsx";

// Excel のオブジェクトは参照したら必ず解放する必要があります。
// 解放しないと Excel のプロセスが残り続けます。
dynamic? excel = null;
dynamic? books = null;
dynamic? book = null;
dynamic? sheets = null;
dynamic? sheet = null;
dynamic? cells = null;
dynamic? range1 = null;
dynamic? range2 = null;
try
{
  // Excel.Application の Type を取得
  var type = Type.GetTypeFromProgID("Excel.Application");
  if (type == null)
  {
    Console.WriteLine("Excel がインストールされていません。");
    return;
  }

  // Excel アプリケーションを生成(起動)します
  excel = Activator.CreateInstance(type);
  if (excel == null)
  {
    Console.WriteLine("Excel.Application のインスタンスの生成に失敗しました。");
    return;
  }
  excel.DisplayAlerts = false;

  // ワークブック一覧を参照します。参照する場合は必ず変数に保持します
  books = excel.Workbooks;

  // Excel ファイルを開きます
  book = books.Open(excelFilePath);

  // シート一覧を参照するので変数に保持します
  sheets = book.Worksheets;

  // シートを参照します。最初のシートは 1 になります
  sheet = (Microsoft.Office.Interop.Excel.Worksheet)sheets[1];

  // セル一覧を参照します
  cells = sheet.Cells;

  // 左上のセルを参照します。一番左上は [1, 1] になります
  range1 = (Microsoft.Office.Interop.Excel.Range)cells[1, 1];

  // セルの値を取得します。値の取得なので後で解放する必要はありません
  var value = (int)range1.Value;

  // 下のセルを参照します
  range2 = (Microsoft.Office.Interop.Excel.Range)cells[2, 1];

  // 日付分足してセットします
  range2.Value = DateTime.Now.Day + value;

  // 保存して閉じます
  book.Close(SaveChanges: true);

  Console.WriteLine("処理が完了しました。");
}
catch (Exception ex)
{
  // 閉じていなければ保存せずに閉じます
  // 開きっぱなしだと Excel 起動時に保存されていないデータとして表示される場合があります
  if (book != null) book.Close(SaveChanges: false);

  Console.WriteLine("処理が失敗しました。");
  Console.WriteLine(ex);
}
finally
{
  // 終了していなければ終了します
  if (excel != null) excel.Quit();

  // 例外が発生した場合でも必ずリソースを解放するようにします
  if (range1 != null) System.Runtime.InteropServices.Marshal.FinalReleaseComObject(range1);
  if (range2 != null) System.Runtime.InteropServices.Marshal.FinalReleaseComObject(range2);
  if (cells != null) System.Runtime.InteropServices.Marshal.FinalReleaseComObject(cells);
  if (sheet != null) System.Runtime.InteropServices.Marshal.FinalReleaseComObject(sheet);
  if (sheets != null) System.Runtime.InteropServices.Marshal.FinalReleaseComObject(sheets);
  if (book != null) System.Runtime.InteropServices.Marshal.FinalReleaseComObject(book);
  if (books != null) System.Runtime.InteropServices.Marshal.FinalReleaseComObject(books);
  if (excel != null) System.Runtime.InteropServices.Marshal.FinalReleaseComObject(excel);
}

変数の宣言の型が dynamic になっているのと Excel.Application を作成するところが若干変わっていますがそれ以外のコードはほぼ流用出来ています。

上記コードは Excel のライブラリを参照していないので対象の Excel 関連クラス情報を使用することが出来ません。そのため変数の型は全て dynamic にしています。 また、Excel.Application クラスも直接参照できないので代わりに Type.GetTypeFromProgIDExcel.Application の Type 情報を取得し Activator.CreateInstance に渡すことで Excel.Application のインスタンスを生成しています。 ちなみに Excel がインストールされていない環境では Type.GetTypeFromProgIDType を取得できないのでインストールされてるかどうかはここで切り分けは可能です。

dynamic を使うデメリットとしてはプログラム構築時ではクラスを判別できないため、 コード入力時の補完機能であるインテリセンスでメソッド名やプロパティ名を表示できない点があります。 最初の内はライブラリを参照している状態でコードを作成し、後で dynamic に置き換えた方が手間が少ないかもしれません。

Microsoft 365 の環境で実行すると以下のように反映されていることを確認出来ます。

以下はかなり古い Office 2007 で実行した結果です。 特定のバージョンでしか動かない機能を使わなければかなり幅広く動作させることが出来ます。

ちなみに Excel がインストールされていない環境で実行すると処理出来ないことを正しく判定出来ています。