C#メモ Excelファイルの標準モジュールに作ったSubプロシージャやらFunctionプロシージャを呼び出してみる

C#,VBA

野暮用で試したので。

ポイントはこんな感じ。

  • ExcelのVBAでSubプロシージャやFunctionプロシージャはC#から呼び出されるための特別な書き方をしなくてOK
  • Visual Studioのプロジェクト参照設定で"Microsoft Excel X.X Object Library"ってのを追加しておく
    ※X.Xは数値でPCにインストールしてあるExcelのバージョン
  • Microsoft.Office.Interop.Excel.Application.Runメソッドで取り扱うExcelのパスと必要に応じてVBAに渡す引数を指定する
  • C#側でExcelに関係するオブジェクトはSystem.Runtime.InteropServices.Marshal.ReleaseComObjectメソッドで明示的に解放する

準備したサンプルのExcel VBAマクロはこんな感じ。
マクロの動きとしては単にダイアログを表示するだけ。
SubプロシージャとFunctionプロシージャでそれぞれ引数ありなし、それとは別にFunctionプロシージャは戻り値ありを定義。

'
' 引数なしのSub
'
Private Sub ModuleSub()
    MsgBox "標準モジュールの引数なしSubを呼んだよ!"
End Sub
'
' 引数ありのSub
'
Private Sub ModuleSubWithArgument(message As String)
    MsgBox "標準モジュールの引数ありSubを呼んだよ! 引数は..." & message
End Sub
'
' 引数なしのFunction
'
Private Function ModuleFunction()
    MsgBox "標準モジュールの引数なしFunctionを呼んだよ!"
End Function
'
' 引数ありのFunction
'
Private Function ModuleFunctionWithArgument(message As String)
    MsgBox "標準モジュールの引数ありFunctionを呼んだよ! 引数は..." & message
End Function
'
' 戻り値ありのFunction
'
Private Function ModuleFunctionWithReturn()
    Dim result As String
    result = "げろげーろ"
    MsgBox "標準モジュールの戻り値ありFunctionを呼んだよ! 戻り値は..." & result
    ModuleFunctionWithReturn = result
End Function

んで、実際にVBAマクロを呼び出すC#のコードはこんな感じ。

/// <summary>
/// メインメソッド
/// </summary>
/// <param name="arguments">コマンドライン引数(今回は使わない)</param>
static void Main(string[] arguments)
{
    // メソッド共通で使う変数を定義する
    // ※try-catch-finally構文で使いまわしたいのでここで定義する
    Microsoft.Office.Interop.Excel.Application application = null;
    Microsoft.Office.Interop.Excel.Workbooks workbooks = null;
    string location = @"C:\Users\tetsuyanbo\Desktop\Sample.xlsm";
    string target = string.Empty;
    string parameter = string.Empty;
    var result = string.Empty;
    // 処理を実行する
    try
    {
        // Excelを起動する
        application = new Microsoft.Office.Interop.Excel.Application();
        application.DisplayAlerts = false;    // Excelのメッセージは抑制する
        // ワークブックを開く
        workbooks = application.Workbooks;
        workbooks.Open(location);
        // 標準モジュールの引数なしSubを実行する
        target = System.IO.Path.GetFileName(location) + "!" + "GeneralModule.ModuleSub";
        parameter = "なし";
        result = "なし";
        application.Run(target);
        Console.WriteLine("呼び出す文字列 : {0} 引数 : {1} 戻り値: {2}", target, parameter, result);
        // 標準モジュールの引数ありSubを実行する
        target = System.IO.Path.GetFileName(location) + "!" + "GeneralModule.ModuleSubWithArgument";
        parameter = "わっほい";
        result = "なし";
        application.Run(target, parameter);
        Console.WriteLine("呼び出す文字列 : {0} 引数 : {1} 戻り値: {2}", target, parameter, result);
        // 標準モジュールの引数なしFunctionを実行する
        target = System.IO.Path.GetFileName(location) + "!" + "GeneralModule.ModuleFunction";
        parameter = "なし";
        result = "なし";
        application.Run(target);
        Console.WriteLine("呼び出す文字列 : {0} 引数 : {1} 戻り値: {2}", target, parameter, result);
        // 標準モジュールの引数ありFunctionを実行する
        target = System.IO.Path.GetFileName(location) + "!" + "GeneralModule.ModuleFunctionWithArgument";
        parameter = "わっほい";
        result = "なし";
        application.Run(target, parameter);
        Console.WriteLine("呼び出す文字列 : {0} 引数 : {1} 戻り値: {2}", target, parameter, result);
        // 標準モジュールの戻り値ありFunctionを実行する
        target = System.IO.Path.GetFileName(location) + "!" + "GeneralModule.ModuleFunctionWithReturn";
        parameter = "なし";
        result = application.Run(target);
        Console.WriteLine("呼び出す文字列 : {0} 引数 : {1} 戻り値: {2}", target, parameter, result);
    }
    catch(Exception exception)
    {
        // 例外の場合はメッセージをコンソールに表示する
        Console.WriteLine(exception.Message);
    }
    finally
    {
        // 実行で使ったCOMオブジェクトを破棄する
        if(workbooks != null)
        {
            workbooks.Close();
            System.Runtime.InteropServices.Marshal.ReleaseComObject(workbooks);
        }
        if(application != null)
        {
            application.Quit();
            System.Runtime.InteropServices.Marshal.ReleaseComObject(application);
        }
    }
}

で、実行してみるとこんな感じ。
まずは、動作としてはこんな感じでダイアログが順番に表示される。

  1. 引数なしのSubプロシージャを呼び出したときに表示されるダイアログはこんな感じ。
    Subプロシージャが呼び出されとる。
  2. 引数ありのSubプロシージャを呼び出したときに表示されるダイアログはこんな感じ。
    C#側から渡した引数はVBA側で受け取れているみたい。
  3. 引数なしのFunctionプロシージャを呼び出したときに表示されるダイアログはこんな感じ。
    Functionプロシージャが呼び出されとる。
  4. 引数ありのFunctionプロシージャを呼び出したときに表示されるダイアログはこんな感じ。
    Subプロシージャと同じくC#側から渡した引数はVBA側で受け取れているみたい。
  5. 戻り値ありのFunctionプロシージャを呼び出したときに表示されるダイアログはこんな感じ。
    戻り値云々のメッセージを表示しているけど実際に戻り値がC#側で受け取れたかは呼び出し側で確認することになるので、ここでは戻り値がどんなものかの確認まで。

コンソールはこんな感じで表示される。
VBAのFunctionプロシージャで返した戻り値もちゃんとC#側で受け取れとる(5行目)みたい。

ちなみに、C#側でSubなりFunctionのプロシージャの定義と合わなかったりする(引数の指定がないとか)と、こんな感じのメッセージでC#側で例外が発生するので注意。

指定されたパラメーターの数が期待された数と一致しませんでした。

今回のコードだと確認用に表示してるからダイアログを閉じないと処理が進まない(C#側に状態遷移しない)けど、VBA側で表示するダイアログがいらんかったらMicrosoft.Office.Interop.Excel.Application.DisplayAlertsプロパティをfalseに設定して抑制するんかなぁ?

そんなこんなで、明日への自分へのメモってことで。

CSharp(CallExcelMethodOnGeneralModule)