xlwingsを使ってPythonからExcel VBAマクロを呼び出す

xlwingsのUDFの実用的な使い方として、PythonからExcel VBAマクロを呼び出す方法を説明します。「xlwingsとは何ぞや？」という方には、PythonのインストールからxlwingsのUDFの基本的な使い方までを以下で投稿していますので、そちらをご覧ください。
[VBAユーザーのためのPython入門 ～xlwingsでExcelからPythonを呼び出す～](https://qiita.com/k_maki/items/9efdb70f69d6f3b00f80)
# 1. 背景
脱Excel VBAを目指してPythonで社内EUCツールを作り始めたとしても、社内には先人たちが作ったExcel VBAツールが山のようにあり、それらも使用しなければならないということがあると思います。VBAが嫌いだからと言って捨てる訳にも行かず、いっそのことPythonで書き直そうかと思っても忙しいし量が多くてそれもできず。結局はVBAからは逃れられないのかと頭を抱えます。

そして、一連の作業で複数のExcel VBAツールを使う場合は、たいてい都度ファイルを開きマクロを実行して、終わったら次のファイルという流れになるかと。じゃあ、自動的にVBAマクロを次々と実行するプログラムをVBAで作ればいいじゃんとなるかもしれません。

いや、やっぱり止めましょうVBA。それ、Pythonでできますから。

本記事では、PythonからExcel VBAマクロを実行する方法を説明します。この方法なら、既存のVBA資産を活かしつつ、Pythonで快適にコーディングできるようになります。

# 2. 環境
以下の環境で試しています。

- OS: Windows 10 version 1909
- Python: version 3.7.5
- xlwings: version 0.18.0
- エディター: VSCode version 1.45.1 & Python拡張機能 version 2020.5.80290



# 3. 基本
[公式ドキュメント（Python API）](https://docs.xlwings.org/ja/latest/api.html?highlight=macro#xlwings.Book.macro)を参考に、PythonからExcel VBAマクロを呼び出してみましょう。

適当なフォルダーで、`VBA_example.xlsm`というファイルを作成し、`Alt+F11`でVBAエディターを開きます。適当なモジュール（とりあえずModule1とします）を追加し、以下を記述します。

```VB:VBA_example.xlsm!Module1
Sub hello_VBA()
    MsgBox "Hello VBA from Python."
End Sub
```

次に、同じフォルダーをVisual Studio Codeで開き、`call_VBA.ipynb`というファイルを作成します。`call_VBA.ipynb`で以下を（できれば一行ずつ）実行します。

```python:call_VBA.ipynb
import xlwings as xw             # 1. xlwingsをインポート
wb = xw.Book('VBA_example.xlsm') # 2. ブックを開く
macro = wb.macro('hello_VBA')    # 3. マクロを取得
macro()                          # 4. マクロを実行
```

コードの説明すると、

1. xlwingsをインポートします。`as xw`としていますので、以下`xw`でxlwingsを呼び出します。
2. 同じフォルダーにあるブックなら`xw.Book`で開けます。
3. ブック内のマクロ`hello_VBA`を変数`macro`に格納します。VBAユーザーだとここで??となるかもしれませんが、Pythonは関数も変数に格納することができます。
4. `macro`を実行します。VBAだと`Call macro`とかになるかもしれませんが、`Call`は不要です。引数が必要なマクロの場合は、括弧内に引数を入れます。

となります。

これをVSCodeで実行中の画面は以下のとおりです:
![ipynb実行画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239172/aa3c3acb-4656-c0d6-37fc-32484a23cef1.png)

最後のセルを実行したとき止まりますが、Excelのウィンドウをアクティブにすると、次の画面が表示されます:
![Hello VBA.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239172/ac404a2b-0e95-e372-3254-f440cf569872.png)

ちゃんとVBAマクロが実行されてますね。

# 4. 応用（VBAマクロの連続実行）
UDFを使った実用的な例です。あるExcelファイルのボタンから、他のExcelファイルのVBAマクロを連続で実行できるようにしてみます。

適当なフォルダーで`VBA_caller.xlsm`を作成します。ワークシートの内容は次のようにします:
![VBA_caller.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239172/becc72fe-c983-5573-21d8-89a6fbc1b39f.png)

- シート名は`VBA_caller`
- テーブル`T.VBA_caller`を作成（ホームタブのテーブルとして書式設定から。場所はどこでもOK。）
- テーブルには列`フォルダー`、`ファイル`、`マクロ`を含むこと。

`Alt+F11`でVBAエディターの画面を開き、参照設定でxlwingにチェックを入れます。Module1を作成し、以下を貼り付けます:

```VB:VBA_caller.xlsm!Module1
Sub main()
  Dim rg_target As Range
  Set rg_target = Worksheets("VBA_caller").ListObjects("T.VBA_caller").Range
  
  Application.DisplayAlerts = False
  var_log = xlwings_udfs.call_vba(rg_target)
  Application.DisplayAlerts = True
End Sub

```




Pythonのスクリプトを準備します。同じフォルダーに以下の内容のファイル`VBA_caller.py`を作成します。

```python:VBA_caller.py
import os
from pathlib import Path

import numpy as np
import pandas as pd
import xlwings as xw


os.chdir(Path(__file__).parent.absolute().resolve())

@xw.sub
@xw.arg('df_target', pd.DataFrame, index=False, header=True)
def call_vba(df_target: pd.DataFrame):

    def call_vba_single(row):
        #設定読み込み
        wb_path, wb_name, macro_name = row['フォルダー'], row['ファイル'], row['マクロ']

        #マクロ実行用Excelインスタンス生成し、ブックを開く
        app = xw.apps.add()
        xl_path  = (Path() / wb_path / wb_name).resolve()
        wb = app.books.open(str(xl_path))

        #マクロ実行
        app.display_alerts = False #マクロ実行中に、Excelから「別のプログラムでOLEの操作が完了するまで待機します。」というメッセージが表示され、実行が止まるため
        wb.macro(macro_name)()
        app.display_alerts = False #呼び出し先でTrueに戻されることがあるので、再びFalseに設定

        #ブックを保存し、マクロ実行用ExcelインスタンスをKill
        wb.save() 
        wb.close()
        app.kill()

    for _, row in df_target.iterrows():
        call_vba_single(row)

@xw.sub
def kill_excel():
    os.system('taskkill /f /im excel.exe')

#デバッグ用
if __name__ == '__main__':
    xw.serve()
```

`VBA_caller.py`作成後、`VBA_caller.xlsm`に戻り、xlwingsタブの`Import Functions`ボタンを押し、`VBA_caller.py`の内容をUDFとして取り込みます。これで、呼び出し元の準備は終了です。

実験用に、呼び出し先のExcelファイルも準備してみましょう。

```:フォルダー構成
┌example1
│　└example1.xlsm
├example2
│　├example2.xlsm
│　└example3
│　　　└example3.xlsm
├VBA_caller.py
└VBA_caller.xlsm
```

`example1.xlsm`～`example3.xlsm`には標準モジュールに以下を保存しておきます:

```VB:example1.slsm～example3.xlsm!Module1
'プロシージャ‐名とMsgBoxの文字列は適宜変更
Sub example1()
    MsgBox "example1"
End Sub
```

これで準備ができました。

実行してみます。`VBA_caller.xlsm`で`Alt+F8`を押し、`main`を選択し、実行ボタンを押します。新しいExcelインスタンスで`example1.xlsm`が開き、メッセージボックスに`example1`と表示されれば成功です。OKボタンを押すと、example2.xlsm、example3.xlsmのマクロが次々実行されます。

# 5. さらに応用（VBAマクロの同時連続実行）
ここまでの内容なら、VBAでもほぼ同じことができます。しかし、`VBA_caller.py`を少し書き換えると、複数マクロの同時実行もできるようになります。

修正その1: imoprt文の最後に以下を追加します。

```python:VBA_caller.py
import joblib
```

修正その2: 以下のforループを削除し、

```python:VBA_caller.py
    for _, row in df_target.iterrows():
        call_vba_single(row)
```

joblibを使った呼び出しに変更します。

```python:VBA_caller.py
    joblib.Parallel(n_jobs=-1, verbose=10)([
        joblib.delayed(call_vba_single)(row) for _, row in df_target.iterrows()
    ])
```

修正(?)その3: （2までで上手く行かなかったら試してください）xlwingsタブの`Interpreter`にpython.exeのパスを入力します。Anacondaをデフォルト設定でインストールしていれば、`C:\ProgramData\Anaconda3\python.exe`になります。

`VBA_caller.xlsm`に戻り、再度`Alt+F8`から`main`を実行してみましょう。今度は複数のExcelが同時に立ち上がり、マクロが同時実行されます。これはVBAには（たぶん）できないことであり、複数のマクロを同時実行することで、全体の処理時間を短くすることができます。

joblibはPythonで簡単に並列処理をできるようにするためのライブラリーで、詳しい説明は[こちら](https://qiita.com/Yuhsak/items/1e8533343cf5458e2e08)などをご覧ください。

# おわりに
最後まで読んでいただき、ありがとうございます。
今回は基本的な使い方ということで、引数が必要になるようなVBAマクロの呼び出しについては書きませんでしたが、必要でしたらその旨コメントいただければと思います。

# 参考
- [公式ドキュメント](https://docs.xlwings.org/ja/latest/api.html#xlwings.Book.macro)
- [VBAユーザーのためのPython入門 ～xlwingsでExcelからPythonを呼び出す～](https://qiita.com/k_maki/items/9efdb70f69d6f3b00f80)