Running Object Tableからオブジェクトを取得する（例：開かれているExcelのブックの一覧を取得する）

Running Object Table（ROT）に関して調べてみたことのメモ。

ざっくりとした理解なので、間違いなどあれば指摘していただけるとうれしいです。

参考サイト
> [モニカ EternalWindows](http://eternalwindows.jp/com/moniker/moniker00.html "モニカ")
> [複数の EXCEL.EXE が動いている時に、ひとつの Excel.Application を GetObject で特定して C# で操作するには？ - QA@IT](https://qa.atmarkit.co.jp/q/4634 "複数の EXCEL.EXE が動いている時に、ひとつの Excel.Application を GetObject で特定して C# で操作するには？ - QA@IT")

# 概念

## Running Object Table

その端末上で実行中（Running）のCOMオブジェクト（Object）の一覧（Table）。

VBAなどの[GetObject 関数](https://msdn.microsoft.com/ja-jp/vba/language-reference-vba/articles/getobject-function "GetObject 関数")、.NET Frameworkの [Marshal.BindToMoniker](https://msdn.microsoft.com/ja-jp/library/system.runtime.interopservices.marshal.bindtomoniker%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396 "Marshal.BindToMoniker メソッド (String) (System.Runtime.InteropServices)")・[Marshal.GetActiveObject](https://msdn.microsoft.com/ja-jp/library/system.runtime.interopservices.marshal.getactiveobject(v=vs.110).aspx "Marshal.GetActiveObject メソッド (String) (System.Runtime.InteropServices)") などはここにあるオブジェクトを取得している。

## モニカ（Moniker）

ROT内のオブジェクトなどを特定するのに使われるもの。
文字列で表現でき、`GetObject`の第1引数や`BindToMoniker`に渡すものに相当する。

いくつか種類があり、下の記事で使用しているのはそのうちのファイルモニカとなる。

> [GetObject(, "Excel~")だけで無く、GetObject(ファイル名)も活用しよう - Qiita](https://qiita.com/nukie_53/items/12cc0a3fc295a446a045)

# コード

「開かれているExcelのブックの一覧を取得する」を目的にコードを作成した。

Excelの場合、ブックを開いている→ファイルモニカがROTに登録されているとなっていることが多い[^1]。
そのため、ROTからファイルモニカで登録されているオブジェクトを探し、さらにそれらの中から`Excel.Workbook`にキャストできるものを探している。

[^1]: ローカルドライブやUNCパスの範囲内ならROTに登録されている。OneDrive上などネットワーク上だと見つけられなかった。

> [複数の EXCEL.EXE が動いている時に、ひとつの Excel.Application を GetObject で特定して C# で操作するには？ - QA@IT](https://qa.atmarkit.co.jp/q/4634 "複数の EXCEL.EXE が動いている時に、ひとつの Excel.Application を GetObject で特定して C# で操作するには？ - QA@IT")

にあった「BindToObject の実行時に例外が起こることがあります」の問題はつぶしたつもり。


C#はあまり詳しくないので若干レガシーな書き方だと思われる。

```csharp:RunningObjectTableManager.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;

using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using Excel = Microsoft.Office.Interop.Excel;

/// <summary>
/// Running Object Table関連操作のラッパークラス
/// </summary>
public class ROTManager
{
    /*
     * 参考サイト
     * モニカの概念について
     * [モニカ](http://eternalwindows.jp/com/moniker/moniker00.html)
     * 実際のC#の実装例
     * [複数の EXCEL.EXE が動いている時に、ひとつの Excel.Application を GetObject で特定して C# で操作するには？ - QA@IT]
     * (https://qa.atmarkit.co.jp/q/4634)
     */

    public static Excel.Workbook[] GetOpendLocalWorkbooks()
    {
        return GetOpendComObjects<Excel.Workbook>();
    }

    /// <summary>
    /// ファイルモニカを元に取得できるオブジェクトから、指定した型のオブジェクトを探す
    /// </summary>
    /// <typeparam name="T">取得したいCOMの型</typeparam>
    /// <returns>型変換に成功したオブジェクト</returns>
    public static T[] GetOpendComObjects<T>() where T : class
    {
        List<T> ts = new List<T>();
        // OfType<T>は解放のタイミングが難しそう
        foreach (var o in GetRunningObjects(MKSYS.MKSYS_FILEMONIKER))
        {
            T t = o as T;
            if (t != null)
            {
                ts.Add(t);
            }
            else
            {
                Marshal.FinalReleaseComObject(o);
            }
        }
        return ts.ToArray();
    }

    /// <summary>
    /// モニカーの種類
    /// </summary>
    /// <see cref="https://docs.microsoft.com/ja-jp/windows/desktop/api/objidl/ne-objidl-tagmksys"/>
    enum MKSYS
    {
        MKSYS_NONE,
        MKSYS_GENERICCOMPOSITE,
        MKSYS_FILEMONIKER,
        MKSYS_ANTIMONIKER,
        MKSYS_ITEMMONIKER,
        MKSYS_POINTERMONIKER,
        MKSYS_CLASSMONIKER,
        MKSYS_OBJREFMONIKER,
        MKSYS_SESSIONMONIKER,
        MKSYS_LUAMONIKER
    }

    /// <summary>
    /// Returns a pointer to an implementation of IBindCtx (a bind context object). This object stores information about a particular moniker-binding operation.
    /// </summary>
    /// <param name="reserved">This parameter is reserved and must be 0.</param>
    /// <param name="ppbc">Address of an IBindCtx* pointer variable that receives the interface pointer to the new bind context object. When the function is successful, the caller is responsible for calling Release on the bind context. A NULL value for the bind context indicates that an error occurred.</param>
    /// <returns>This function can return the standard return values E_OUTOFMEMORY and S_OK</returns>
    /// <see cref="https://docs.microsoft.com/en-us/windows/desktop/api/objbase/nf-objbase-createbindctx"/>
    [DllImport("ole32.dll")]
    private static extern int CreateBindCtx(uint reserved, out IBindCtx ppbc);

    /// <summary>
    /// Running Object Table から指定されたモニカのオブジェクトを取得する
    /// </summary>
    /// <param name="monikerType">モニカの種類</param>
    /// <returns>見つかったオブジェクト</returns>
    private static object[] GetRunningObjects(MKSYS monikerType)
    {
        // Running Object Table を取得する
        const uint reserved = 0;
        IBindCtx ctx;
        CreateBindCtx(reserved, out ctx);

        IRunningObjectTable runningObjectTable;
        ctx.GetRunningObjectTable(out runningObjectTable);

        IEnumMoniker enumMoniker;
        runningObjectTable.EnumRunning(out enumMoniker);
        // ここまではほぼ定型
            
        enumMoniker.Reset();
        List<object> returnObjects = new List<object>();

        while (true) 
        {
            const int S_OK = 0;

            IMoniker[] tmpMks = new IMoniker[1];
            IntPtr fetched = IntPtr.Zero;
            // bufMonikers の数ずつモニカーを取得
            bool successNext = enumMoniker.Next(tmpMks.Length, tmpMks, fetched) == S_OK;
            if (!successNext)
            {
                break;
            }

            try
            {
                //for Debug
                string dispName;
                tmpMks[0].GetDisplayName(ctx, null,out dispName);
                Debug.WriteLine("DisplayName\t" + dispName);

                Guid clsId;
                tmpMks[0].GetClassID(out clsId);
                Debug.WriteLine(clsId);
                // Debug.WriteLine(Marshal.GetTypeFromCLSID(clsId)); //-> System.__ComObject

                int pdwMksys;
                if (tmpMks[0].IsSystemMoniker(out pdwMksys) != S_OK)
                {
                    Debug.WriteLine("not SystemMoniker");
                    continue;
                }

                MKSYS mkType = (MKSYS)Enum.ToObject(typeof(MKSYS), pdwMksys);
                Debug.WriteLine(mkType);
                if (mkType != monikerType)
                {
                    continue;
                }

                object obj;
                if (runningObjectTable.GetObject(tmpMks[0], out obj) != S_OK)
                {
                    continue;
                }
                returnObjects.Add(obj);
            }
            finally
            {
                Marshal.FinalReleaseComObject(tmpMks[0]);
            }
        }

        Marshal.FinalReleaseComObject(enumMoniker);
        Marshal.FinalReleaseComObject(runningObjectTable);
        Marshal.FinalReleaseComObject(ctx);

        return returnObjects.ToArray();
    }
}
```

## 使用例

### C# #

```csharp
using System;
using System.Runtime.InteropServices;
using Excel = Microsoft.Office.Interop.Excel;

public class Program
{
    static void Main(string[] args)
    {
        foreach (var wb in ROTManager.GetOpendLocalWorkbooks())
        {
            Console.WriteLine(wb.Name);
            Marshal.FinalReleaseComObject(wb);
        }
        Console.WriteLine("Finish");
        Console.ReadKey();
    }
}
```

## PowerShell

上記のRunningObjectTableManagerをどこかに○○.csで保存して以下を実行。
ファイルとして保存する代わりに`TypeDefinition`にコード文字列を指定しても動くはず。

```powershell
[hashtable]$addTypeParams = @{
    Path = 上記のサンプルをどこかに○○.csで保存したパス
    ReferencedAssemblies = @('Office', 'Microsoft.Office.Interop.Excel')
}
Add-Type @addTypeParams

foreach ($wb in [ROTManager]::GetOpendLocalWorkbooks()) {
    $wb.Name
    [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($wb) > $null
}
```

# 参考

> [モニカ EternalWindows](http://eternalwindows.jp/com/moniker/moniker00.html "モニカ")
> [複数の EXCEL.EXE が動いている時に、ひとつの Excel.Application を GetObject で特定して C# で操作するには？ - QA@IT](https://qa.atmarkit.co.jp/q/4634 "複数の EXCEL.EXE が動いている時に、ひとつの Excel.Application を GetObject で特定して C# で操作するには？ - QA@IT")

関連COMインターフェイス

> [IBindCtx | Microsoft Docs](https://docs.microsoft.com/en-us/windows/desktop/api/objidl/nn-objidl-ibindctx "IBindCtx | Microsoft Docs")
> [IRunningObjectTable | Microsoft Docs](https://docs.microsoft.com/ja-jp/windows/desktop/api/objidl/nn-objidl-irunningobjecttable "IRunningObjectTable | Microsoft Docs")
> [IEnumMoniker | Microsoft Docs](https://docs.microsoft.com/ja-jp/windows/desktop/api/objidl/nn-objidl-ienummoniker "IEnumMoniker | Microsoft Docs")
> [IMoniker | Microsoft Docs](https://docs.microsoft.com/ja-jp/windows/desktop/api/objidl/nn-objidl-imoniker "IMoniker | Microsoft Docs")
> [tagMKSYS | Microsoft Docs](https://docs.microsoft.com/ja-jp/windows/desktop/api/objidl/ne-objidl-tagmksys "tagMKSYS | Microsoft Docs")