【VBA】ExcelVBAから外部プログラムを呼び出して内容を処理する

Excel で VBA を使って外部のプログラムを呼び出す方法(と、そのプログラムから Excel の内容を読み取って処理する方法)の紹介です。

Excel 内の VBA にやりたいことを全部実装したほうが使い勝手はよいですが少しデメリットがあって(言語と環境が古すぎて開発効率が悪め、Excel ファイルだから Git などのバージョン管理で扱いが難しい、メンテは結構大変で変更が難しくなる)簡単に始められますが時間が経つと大変になるパターンが多いと思います。

そこで、今回は、Excel の内容を外部プログラム(なんでもいいですが今回は C#)で読み取って内容を処理して外部に結果を出力する方法を紹介したいと思います。

注意:今回開いている Excel の内容を外部で処理するので処理結果を書き戻すことはできません。開いているExcel自身を書き換える処理はVBAにしかできませんがこの記事では扱いません。

VBAで外部プログラムを呼び出す

まず、以下のスクリプトを Excel にマクロとして貼り付けます。

'----------------------------------------------------------------------
' RunExternalProgramWithArguments
'
' 概要:
'   A1にある文字列を外部プロセスのパスとして呼び出しを行います。
'   第1引数に自分自身のファイルパスを渡します。
'
' 補足:
'   どのセルを読むかはマクロを設定するときに以下のように指定する
'   Sample.xlsm!'Sheet1.ExecExtProgram "A1"'
'     1) 関数名~引数までをシングルクォーテーションで囲う
'     2) 引数はダブルクオーテーションで囲う
'
' 引数:
'   なし
'
' 戻り値:
'   なし
'----------------------------------------------------------------------
Sub ExecExtProgram(cellId As String)
    Dim ProgramPath As String
    Dim Arguments As String

    ' 指定された位置の値をファイルパスとして取得する
    ProgramPath = Range(cellId).Value

    ' ダブルクォーテーションで囲う
    If Left(ProgramPath, 1) <> """" Then
        ProgramPath = """" & ProgramPath
    End If
    
    If Right(ProgramPath, 1) <> """" Then
        ProgramPath = ProgramPath & """"
    End If

    ' Excelファイル自身のフルパス
    Arguments = GetMyFilePathWithDoubleQuotation

    ' 外部プロセスの呼び出し
    Call Shell(ProgramPath & " " & Arguments, vbNormalFocus)
End Sub

'----------------------------------------------------------------------
' GetMyFilePathWithDoubleQuotation
'
' 概要:
'   このExcelファイルのフルパスを取得します。
'
' 引数:
'   なし
'
' 戻り値:
'   ダブルクォーテーションで囲まれたファイルパスの文字列
'   "C:\MyDir\MyFile.xlsm"
'----------------------------------------------------------------------
Function GetMyFilePathWithDoubleQuotation() As String
    GetMyFilePathWithDoubleQuotation = _
        """" & ThisWorkbook.Path & Application.PathSeparator & ThisWorkbook.Name & """"
End Function

この状態で Excel にボタンを配置し「Sample.xlsm!'Sheet1.ExecExtProgram "A1"'」のように読みたいセルの位置を引数として指定してマクロを登録します。そうすると指定したセルは外部プロセスのパスとして自分自身のファイルパスを外部のプログラムに引数として渡して実行します。

呼び出される側の実装(C#)

呼び出されたほうは、第1引数に Excel のパス渡されるためこれを Excel を読み取るためのライブラリ「ExcelDataReader」を用いて内容を読み取りたいと思います。

C# の実装環境は以下の通りです

  • VisualStudio 2022
  • .NET 8.0

まず、ExcelDataReader を導入します。

VisualStudioの上部メニューから

ツール > NuGet パッケージ マネージャー > パッケージ マネージャー コンソール

を選択し、コンソールを開いて以下の 2つを入力します。

Install-Package ExcelDataReader -Version 3.6.0
Install-Package ExcelDataReader.DataSet -Version 3.6.0

Excel 内の Sheet2 に以下の定義があるとします。

// Sheet2

| No  | Date             | String                                |
|-----|------------------|---------------------------------------|
| 1   | 2017/6/12 16:45  | 6c6f125d-1349-430a-bbce-56cf867ea27f  |
| 2   | 2020/3/27 11:05  | 99e4f58b-7466-4422-81e6-efe10390ecdc  |
| 3   | 2015/9/19 13:24  | 5485cd79-5b1f-470b-8599-8939d02d7f61  |
| 4   | 2023/1/7 22:33   | 9572927a-8c4f-4e48-8b1b-f7fa505d24ec  |
| 5   | 2012/12/21 8:16  | 9572927a-8c4f-4e48-8b1b-f7fa505d24ec  |

次に Program.cs を以下の通り実装します。

// Program.cs

// 
// 概要:
// 動作確認のためシートの内容を読み取って
// Sample クラスに内容を全部読み取ります
// 

using System.Data;
using System.Diagnostics;
using System.Text;
using ExcelDataReader;

// Shift-JIS対応
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

// 第1引数にExcelのファイルパスが入ってる
string filePath =
    !Debugger.IsAttached ?
        args[0] :
        @"C:\Users\f3275\Desktop\Sample.xlsm"; // デバッグ時の代替パス

// 開かれているファイルをオープンする
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var excel = ExcelReaderFactory.CreateReader(fs);

// Sheet2を選択する
var dataSet = excel.AsDataSet();
var sheet2 = dataSet.Tables["Sheet2"];

// Sheet2の内容を全部列挙する
IEnumerable<Sample> f()
{
    DataRowCollection rows = sheet2.Rows; // 1行目のヘッダーは読み飛ばす
    for (int i = 1; i < rows.Count; i++)
    {
        DataRow row = rows[i];
        int id = Convert.ToInt32(row[0]);
        DateTime time = Convert.ToDateTime(row[1]);
        string name = Convert.ToString(row[2]);
        yield return new(id, time, name);
    }
}

// 読み取った内容をコンソールに全部出力する
foreach (Sample sample in f())
{
    Console.WriteLine($"{sample.ID}, {sample.Time}, {sample.Name}");
}

Console.WriteLine("[End]");

/// <summary>
/// 読み取った1行のデータを保持するクラス
/// </summary>
internal class Sample(int id, DateTime time, string name)
{
    public readonly int ID = id;
    public readonly DateTime Time = time;
    public readonly string Name = name;
    public override string ToString()
    {
        $"{nameof(ID)}={ID}, {nameof(Time)}={Time}, {nameof(Name)}={Name}";
    }
}

これでExcel上にあるボタンを押すと以下のようにコンソールに内容が(一瞬だけ)表示されます。

1, 2017/06/12 16:45:30, 6c6f125d-1349-430a-bbce-56cf867ea27f
2, 2020/03/27 11:05:42, 99e4f58b-7466-4422-81e6-efe10390ecdc
3, 2015/09/19 13:24:56, 5485cd79-5b1f-470b-8599-8939d02d7f61
4, 2023/01/07 22:33:10, 9572927a-8c4f-4e48-8b1b-f7fa505d24ec
5, 2012/12/21 8:16:50, 9572927a-8c4f-4e48-8b1b-f7fa505d24ec

これで、あとは目的のデータ処理を追加で実装することができます。