C#からExcelを操作する方法
実現したいこと
- C#からExcelを起動したい。または起動中のExcelを操作したい。
- C#からExcelアドインのワークシート関数やVBA関数を実行したい。
- C#からExcelのセルの値変化を検知したい。またはセルの値変化時にExcelからC#に通知したい。
- C#からExcelのDDE通信で受信した値を取得したい。
ググる
- 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の仕組み」が分かりやすい。
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オブジェクトを取得できた。
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);
}
}
ようやくスタートライン…。
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関数はどちらの動作速度が速いのだろう。
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 ExcelApp xl = new();
これでメインプログラム側のtry~finallyによるCOMオブジェクト解放処理は不要になり、ExcelApp内の記述だけで完結できる。面倒がらずにIDisposableを調べて良かった!
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して成功するまで繰り返す。