Open7

C#からExcelを操作する方法

idumistidumist

実現したいこと

  1. C#からExcelを起動したい。または起動中のExcelを操作したい。
  2. C#からExcelアドインのワークシート関数やVBA関数を実行したい。
  3. C#からExcelのセルの値変化を検知したい。またはセルの値変化時にExcelからC#に通知したい。
  4. C#からExcelのDDE通信で受信した値を取得したい。
idumistidumist

ググる

  • Excelファイル操作であれば、NPOI、EPPlus、ClosedXMLといったライブラリを使う方法が一般的なようだ。
  • 何故ならMicrosoft.Office.Interop.ExcelのCOM参照は危険がいっぱいだかららしい。
  • しかし、今回はファイル操作ではなくExcelアドイン関数の実行なのでCOM参照しか無さそう?

C#でExcel操作をする時に注意すること【俺みたいになるな!!】
Excelファイルを C# と VB.NET で読み込む "正しい" 方法
.NETを使った別プロセスのOfficeの自動化が面倒なはずがない―そう考えていた時期が俺にもありました。 →‘1 dot good, 2 dots bad’

.NETからのCOMオブジェクトの扱いについては.NET FrameworkからのExcel操作の「Appendix. COMの仕組み」が分かりやすい。

idumistidumist

1. C#からExcelを起動したい。または起動中のExcelを操作したい。

空のxlsxファイルを指定してExcelを起動。

Microsoft.Office.Interop.Excel.Application xlApp = null;
Workbooks xlBooks = null;
Workbook xlBook = null;

try
{
    xlApp = new();
    xlApp.Visible = true;

    xlBooks = xlApp.Workbooks;
    xlBook = xlBooks.Open("z:\\Book1.xlsx");

    MessageBox.Show("起動しました");
}
finally
{
    if (xlBook != null)
    {
        xlBook.Close(false);
        Marshal.ReleaseComObject(xlBook);
    }
    if (xlBooks != null) Marshal.ReleaseComObject(xlBooks);
    if (xlApp != null)
    {
        xlApp.Quit();
        Marshal.ReleaseComObject(xlApp);
    }
}

うーん、起動&正常終了(=タスクマネージャにEXCEL.EXEのプロセスが残らない)はするがアドインを読み込んでくれない。

とりあえず、アドインを読み込んだ状態で起動中のExcelを使うことにする。起動中のExcel COMオブジェクトを取得するにはMarshal.GetActiveObject()を使う。

//xlApp = new();
//xlApp.Visible = true;
xlApp = (Microsoft.Office.Interop.Excel.Application)Marshal.GetActiveObject("Excel.Application");

エラー CS0117 'Marshal' に 'GetActiveObject' の定義がありません

えー。どうやら.NET CoreにはGetActiveObject()が実装されていない。
Powershell 6以降でもGetActiveObject()でExcelを呼ぶ方法
No definition found for GetActiveObject from System.Runtime.InteropServices.Marshal C#

Stack Overflowのソースを元にMarshal2クラスを作成しGetActiveObject()を実装する。

//xlApp = new();
//xlApp.Visible = true;
xlApp = (Microsoft.Office.Interop.Excel.Application)Marshal2.GetActiveObject("Excel.Application");

これでOK。アドインが有効なExcel COMオブジェクトを取得できた。

idumistidumist

1. (続き)C#からExcelを起動してアドインも自動で読み込みたい。

Excelのオプションで該当アドインは有効化済みなので、Excelを手動起動するとアドインも読み込まれる(当該アドインのリボンが生成されるのですぐわかる)。しかし、Open()ではアドインは読み込まれない。

PowershellでのExcelアドイン自動読み込みがExcel2010で失敗する

Open()後にAddInsを取得→Add()を実行することでアドインが読み込まれる。ここでも‘1 dot good, 2 dots bad’がポイント。

Microsoft.Office.Interop.Excel.Application xlApp = null;
Workbooks xlBooks = null;
Workbook xlBook = null;
AddIns xlAddIns = null;
AddIn xlAddIn = null;

try
{
    xlApp = new();
    xlApp.Visible = true;

    xlBooks = xlApp.Workbooks;
    xlBook = xlBooks.Open("z:\\Book1.xlsx");

    xlAddIns = xlApp.AddIns;
    xlAddIn = xlAddIns.Add("z:\\AddIn.xll");
    xlAddIn.Installed = true;

    MessageBox.Show("起動しました");
}
finally
{
    if (xlAddIn != null)
    {
        xlAddIn.Installed = false;  // これを実行しないとCOMオブジェクトが開放されないことがある(?)
        Marshal.ReleaseComObject(xlAddIn);
    }
    if (xlAddIns != null) Marshal.ReleaseComObject(xlAddIns);
    if (xlBook != null)
    {
        xlBook.Close(false);
        Marshal.ReleaseComObject(xlBook);
    }
    if (xlBooks != null) Marshal.ReleaseComObject(xlBooks);
    if (xlApp != null)
    {
        xlApp.Quit();
        Marshal.ReleaseComObject(xlApp);
    }
}

ようやくスタートライン…。

idumistidumist

2. C#からExcelアドインのワークシート関数やVBA関数を実行したい。

ワークシート関数なら単純にセルに書き込めばOK。即時実行される。セルの書き込み方法は何種類かあるが、セルの読み取りはValueよりValue2が速いらしいので書き込みでも速いだろうとの浅はかな考えでValue2を使う。

C#でExcel操作の処理時間を計測してみた (Microsoft.Office.Interop.Excel)

ここでも‘1 dot good, 2 dots bad’(しつこい)。

Sheets xlSheets = null;
Worksheet xlSheet = null;
//Microsoft.Office.Interop.Excel.Range xlCells = null;  // Cells経由でRangeを使う場合
Microsoft.Office.Interop.Excel.Range xlRange = null;

try
{
    xlSheets = xlBook.Sheets;
    xlSheet = xlSheets[1];

    //xlCells = xlSheet.Cells;  // Cells経由でRangeを使う場合
    //xlRange = xlCells[1, 1];  // Cells経由でRangeを使う場合
    xlRange = xlSheet.Range["A1"];  // 直接Rangeを使う場合

    xlRange.Value2 = "=AddInWorksheetFunc()";
    MessageBox.Show("AddInWorksheetFunc()を実行しました");
}
finally
{
    if (xlRange != null) Marshal.ReleaseComObject(xlRange);
    //if (xlCells != null) Marshal.ReleaseComObject(xlCells);  // Cells経由でRangeを使う場合
    if (xlSheet != null) Marshal.ReleaseComObject(xlSheet);
    if (xlSheets != null) Marshal.ReleaseComObject(xlSheets);
}

これでVBA関数を実行する必要性が(いったん)無くなってしまったので、VBA関数の実行方法は後回し。ところで、アドインのVBA関数をC#から直接呼べたりしないかな。あと、同じ機能のワークシート関数とVBA関数はどちらの動作速度が速いのだろう。

idumistidumist

IDisposableの実装とusingステートメントの利用。

デバッグ中に、EXCEL.EXEプロセスが残ってしまう(=COMオブジェクトが正しく解放できていない)状態に陥るので、

Excelファイルを C# と VB.NET で読み込む "正しい" 方法

何度も使うのであればIDisposableの実装クラスを作成したほうがいいです

との教えからIDisposableを調べる。

[雑記] Dispose にまつわる余談
デストラクタ
C# の Dispose を正しく実装する

IDisposableを継承し、Dispose() と Dispose(bool disposing) と ファイナライザ(デストラクタ)を実装します

とのこと。こんな感じ?

public class ExcelApp : IDisposable
{
    private bool _disposed = false;

    ~ExcelApp()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
    }
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // マネージドリソース用
            }
            // アンマネージドリソース用
            ReleaseExcelObjects();  // 解放できていないCOMオブジェクトの解放処理を呼び出す

            _disposed = true;
        }
    }

    protected void ReleaseExcelObjects()
    {
        // Marshal.ReleaseComObject()のループかMarshal.FinalReleaseComObject()で解放
    }
}

そうするとusing変数宣言が使える。

リソースの破棄:using 変数宣言

using ExcelApp xl = new();

これでメインプログラム側のtry~finallyによるCOMオブジェクト解放処理は不要になり、ExcelApp内の記述だけで完結できる。面倒がらずにIDisposableを調べて良かった!

idumistidumist

2. (続き)セル参照やセル指定でCOMExceptionが発生する

ワークシート関数実行直後、当該セルをValue2で参照するとそれなりの頻度で、

System.Runtime.InteropServices.COMException (0x80010001): 呼び出し先が呼び出しを拒否しました。 (0x80010001 (RPC_E_CALL_REJECTED))
   at System.RuntimeType.InvokeMember(String name, BindingFlags bindingFlags, Binder binder, Object target, Object[] providedArgs, ParameterModifier[] modifiers, CultureInfo culture, String[] namedParams)
   at System.RuntimeType.ForwardCallToInvokeMember(String memberName, BindingFlags flags, Object target, Object[] aArgs, Boolean[] aArgsIsByRef, Int32[] aArgsWrapperTypes, Type[] aArgsTypes, Type retType)
   at Microsoft.Office.Interop.Excel.Range.get_Value2()

という例外が発生する。原因はExcel側がセルを書き換え中だからと思われるが、稀にワークシート関数実行の単なるRange[]によるセル指定でも発生する(なぜ?)。どちらの場合も再度Value2やRange[]を実行すれば問題は無さそうなので、適当な上限を決めたループの中でtry~catchして成功するまで繰り返す。