回答編集履歴

1 VBAプロシージャコールバックのサンプル を追加

imihito

imihito score 2066

2018/08/11 14:29  投稿

よく使われている(私が知っている)COMオブジェクトの範囲では、条件を完全に満たすものは無いと思われます。
ぱっと思いついた方法を簡単にまとめたので参考になれば。
# PowerShellの実行方法
#### `WshShell.Exec()`で実行し、`WshExec`を受け取る
- ウィンドウが一瞬表示される
- 文字で表現出来る情報しか返せない
という点が許容できれば一番無難な方法。
#### `VBA.Shell()`や`WshShell.Run()`で終了を待たずに実行
ウィンドウを完全に非表示にしたい場合。
結果を受け取るには外部の何かを使う必要がある。
終了を待たない場合、`WshShell.Run()`を使う意味はほとんど無い。
# 結果を受け取る方法
`WshExec`を使っている場合はそちらから読み取れば良いので割愛。
#### ファイルに出力
基本文字列情報のやり取り。
ファイル名の生成には`FileSystemObject.GetTempName()`を使う手も(到底一意とは言えませんが)。
#### クリップボード経由で受け取り
ユーザー操作に影響を与える割に扱いにくい。
#### PowerShellから任意のVBAプロシージャをコールバック
非常にデバッグが困難、お互いの結合が強くなりますが一つの手として。
Excel・PowerPoint・Wordなど一部のVBAホストは、COM操作で外部から任意のVBAのプロシージャを実行出来る機能を提供しています。
PowerShellはCOM操作ができるため、上記の機能でVBAに直接情報を送ることが出来ます。
やり取りとしては以下のようなパターンが考えられます。
- 直接VBAの処理を呼ぶ(お互いの依存がより強くなる)
- 適当なSetterを定義しておき、PowerShellはSetter経由で情報を渡す。あとはVBAが適当なタイミングで取りに行く(外乱の影響が大きい)
なお、標準コマンドレットではExcel・Wordの既存のインスタンスを取得できないため、.NETのメソッドを直接呼び出す必要があります(PowerPointはシングルインスタンスなのでOK)。
# 処理の中断方法
**安全性を度外視する**ならPowerShellプロセスの終了が手っ取り早いでしょう。
`WshShell.Exec`なら[Terminate メソッド (WshScriptExec)](https://msdn.microsoft.com/ja-jp/library/cc364387.aspx)が使用できます。
> [Terminate メソッド (WshScriptExec)](https://msdn.microsoft.com/ja-jp/library/cc364387.aspx)
> Terminate メソッドは最後の手段としてのみ使用します。
`WshShell.Run`から実行した場合は不可能ですが、`VBA.Shell`なら返り値がプロセスIDとなります。
そのため、WMIなどを使えば該当プロセスの取得・終了ができるはずです。
プロセスの終了以外となるとPowerShell側からVBAに何か状況を問い合わせる必要があります。
プロセスの終了以外となるとPowerShell側からVBAに何か状況を問い合わせる必要があります。
---
VBAプロシージャコールバックのサンプル(Excel用)
PowerShellを10個起動して、コールバックでそれぞれの番号をDebug.Printさせています。
`AsyncPsExecTest`を実行するとAsyncPsExecTest終了後にバラバラに結果が出力されるのがわかると思います。
```vba
Sub CallBack(a)
   Debug.Print a
End Sub
Sub AsyncPsExecTest()
   Const PS_CMD_BASE = _
           "PowerShell.exe -NoProfile -Command " & """" & _
               "$appXl = [Runtime.InteropServices.Marshal]::BindToMoniker('{0}').Application;" & _
               "$appXl.Run('{0}!CallBack' , {1} )" & """"
   
   Debug.Print "AsyncPsExecTest Start"
   
   Dim i As Long
   For i = 1 To 10
       Dim psCmd As String
       psCmd = VBA.Replace(VBA.Replace( _
           PS_CMD_BASE, _
               "{0}", ThisWorkbook.Name), _
               "{1}", i)
       
       Dim pId As Double
       pId = VBA.Shell(psCmd, vbHide)
       Debug.Print "ProcessID:="; pId, i
   Next i
   Debug.Print "AsyncPsExecTest End"
End Sub
```

思考するエンジニアのためのQ&Aサイト「teratail」について詳しく知る