C#でPowerShellを使ってCOMオブジェクトブラウザを作ってみた

[COMオブジェクトのメンバー一覧を取得する (PowerShell)](https://qiita.com/kob58im/items/ea1ca69b9f898845a71f)
を応用してブラウザつくってみた。
~~※oleview.exeを持ってればいらない説…(まだ使ってないのでわからない)~~

ちなみにCOMオブジェクトでない普通の.Netのクラス(System.IO.Fileとか)であれば、
[C#リフレクションTIPS 55連発](https://qiita.com/gushwell/items/91436bd1871586f6e663)
が参考になります。

# 画面キャプチャ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/6a69cd34-fa1f-b549-9a63-ec203c0e92a9.png)


# ソースコード

```csharp
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;
using System.Management.Automation;


public class ProgIdInfo
{
    private PSObject _psObj; // ProgID レジストリキー情報のPSObject
    private string _ProgID;
    private string _CLSID;
    private string _ValueOfCLSID;
    
    public string ProgID       { get{return _ProgID;} }
    public string CLSID        { get{return _CLSID;} }
    public string ValueOfCLSID { get{return _ValueOfCLSID;} }
    
    // レジストリのキーを返すので取り扱い注意
    // (戻ってきた PSObject に対してSetやDelete等の書き込み系のメソッドを使わないように。)
    //   ProgIDの一覧取得については、PowerShell使わなくてもレジストリ検索を実装すればできなくはない。その方が処理速いかも
    private static Collection<PSObject> GetProgIdCollection()
    {
        using ( var invoker = new RunspaceInvoke() ) {
            return invoker.Invoke(@"dir REGISTRY::HKEY_CLASSES_ROOT\CLSID -Include PROGID -Recurse");
        }
    }
    
    public static List<ProgIdInfo> GetProgIDs()
    {
        var ret = new List<ProgIdInfo>();
        
        var psObjs = GetProgIdCollection();
        
        foreach ( var psObj in psObjs ) {
            ProgIdInfo t = NewProgIdInfo(psObj);
            ret.Add(t);
        }
        
        return ret;
    }
    
    private static ProgIdInfo NewProgIdInfo(PSObject psObj)
    {
        var ret = new ProgIdInfo();
        ret._psObj  = psObj;
        ret._ProgID = "";
        ret._CLSID  = "";
        ret._ValueOfCLSID = "";
        
        ret._ProgID  = PSObject_Invoke_GetValue(psObj); // ProgID の取得
        
        string[] regKeyPathOfProgID = (psObj.Members["Name"].Value??"").ToString().Split('\\');
        // ["Name"] は HKEY_CLASSES_ROOT\CLSID\{xxxx..-xx..}\ProgID が返る
        
        if ( regKeyPathOfProgID.Length >= 2 ) {
            ret._CLSID = regKeyPathOfProgID[regKeyPathOfProgID.Length-2]; // parent of last node
        }
        string parentKey = MyPartialJoin("\\", regKeyPathOfProgID, 1, 1); // 先頭の @"HKEY_CLASSES_ROOT\" と末尾の @"\ProgID" を捨てる
        ret._ValueOfCLSID = GetClassRootRegisterValue(parentKey, ""); // 既定の値を読み出す
        
        return ret;
    }
    
    
    // レジストリアクセス
    private static string GetClassRootRegisterValue(string regKeyPath, string regValueName)
    {
        if ( regKeyPath == null || regKeyPath == "" ) {
            return "";
        }
        
        Microsoft.Win32.RegistryKey regkey = Microsoft.Win32.Registry.ClassesRoot.OpenSubKey(regKeyPath);
        if (regkey == null) {
            return "";
        }
        
        try{
            return (regkey.GetValue(regValueName)??"").ToString();
        }
        finally{
            regkey.Close();
        }
    }
    
    private static string MyPartialJoin(string separator, string[] ss, int startIndex, int numOfOmitLast)
    {
        // CopyメソッドなりLINQなり使った方が綺麗な気がするが、とりあえずこれで。
        int n = ss.Length - startIndex - numOfOmitLast;
        if ( n <= 0 ) { return ""; }
        else {
            string[] ss2 = new String[n];
            for ( int i = startIndex; i < ss.Length - numOfOmitLast; i++) {
                ss2[i-startIndex] = ss[i];
            }
            return String.Join(separator, ss2);
        }
    }
    
    private static string PSObject_Invoke_GetValue(PSObject psObj)
    {
        object t = psObj.Methods["GetValue"].Invoke(new object[]{""}); // 規定値を取得する。 // レジストリアクセスでやってもよい
        return (t??"").ToString(); // Microsoft PenInputPanel ControlがなぜかProgIdの値が空欄なので、処置しないと例外発生する
    }
}


public class ComObjectMember
{
    private PSObject _psObj;
    private string _MemberTypeText;
    private string _ReturnTypeText;
    private string _MemberName;
    private string _DefinitionText;
    
    private static Regex r = new Regex(@"^([^ ]+)");
    
    public string MemberTypeText { get{return _MemberTypeText;} }
    public string ReturnTypeText { get{return _ReturnTypeText;} }
    public string MemberName     { get{return _MemberName;}     }
    public string DefinitionText { get{return _DefinitionText;} }
    
    private static Collection<PSObject> _GetMembers_PS(string comObjectName)
    {
        Regex r = new Regex(@"^[ ._0-9A-Za-z]+$"); // MS Remote など、スペースが入っている輩がいるので注意

        if ( r.IsMatch(comObjectName) ) {  // 危険なパラメータが混入しないように簡易チェック
            string[] source = new string[] {
                "$t = New-Object -ComObject \"" + comObjectName + "\"",
                "Get-Member -InputObject $t"
            }; // memo: ReleaseComObjectしないとよろしくない気がする

            using ( var invoker = new RunspaceInvoke() ) {
                try {
                    Collection<PSObject> tmp = invoker.Invoke(source[0]);
                    Collection<PSObject> result = invoker.Invoke(source[1]);
                    return result;
                }
                catch (System.Management.Automation.CmdletInvocationException e) {
                    // memo:
                    //  VideoRenderCtl.VideoRenderCtl.1 選んだらCreateInstanceに失敗したっぽい例外を吐いたので
                    //  とりあえずcatchしておく
                    Console.WriteLine(e);
                    return new Collection<PSObject>();
                }
            }
        }
        else {
            return new Collection<PSObject>();// 要素数 0 の Collection を返しておく
        }
    }
    
    
    public static List<ComObjectMember> GetMembers(string comObjectName)
    {
        var psObjs = _GetMembers_PS(comObjectName);
        var ret = new List<ComObjectMember>();
        
        foreach ( var psObj in psObjs ) {
            ret.Add(NewComObjectMember(psObj));
        }
        
        return ret;
    }
    
    private static ComObjectMember NewComObjectMember(PSObject psObj)
    {
        var ret = new ComObjectMember();
        ret._psObj = psObj;
        
        // Regex r = new Regex(@"^([^ ]+)");
        // フォーマットが複雑すぎて分解することが困難なため、戻り値の型(っぽい)部分だけを取り出す。
        //   - overloadが1行に入ってたりする (例: System.Random)
        //   - 仮引数名があったりなかったり
        //   - ()がない場合もある
        //   - ()がネストしている場合もある
        //   - 後ろに{get} {set} がつく場合もある

        Match m = r.Match(psObj.Members["Definition"].Value.ToString());
        //Match m = r.Match(psObj.ToString());

        if ( m.Success ) {
            ret._MemberTypeText = (psObj.Members["MemberType"].Value).ToString(); // PSMemberTypes (Method / Property など)
            ret._ReturnTypeText = m.Groups[1].Value;
            ret._MemberName     = psObj.Members["Name"].Value.ToString();
            ret._DefinitionText = psObj.Members["Definition"].Value.ToString();
        }
        else {
            // 想定外の形式の場合は、とりあえずToString()して DefinitionText に入れる
            ret._MemberTypeText = "";
            ret._ReturnTypeText = "";
            ret._MemberName     = "";
            ret._DefinitionText = psObj.ToString();
        }
        
        return ret;
    }
    
}


class MainForm:Form
{
    Button btnGetProgID;
    Button btnGetMembers;
//    CheckBox chkFilter;
    TextBox  txtFilter;
    SplitContainer spl;
    TextBox txtProgID;
    ListView lsvProgID;
    ListView lsvMemb;
    MyListViewContextMenuInfo contextMenuInfo;
    
    List<ProgIdInfo> allProgIDs; // filter処理を入れたいので、ListViewに直接持たせるのはやめた。
    
    
    MainForm()
    {
        Text = "COM Object Browser";
        
        allProgIDs = new List<ProgIdInfo>();

        spl = new SplitContainer();
        spl.Location = new Point(0, 0);
        spl.Dock = DockStyle.Fill;
        spl.Orientation = Orientation.Vertical;
        Controls.Add(spl);
        
        ////// 左 Panel ここから
        btnGetProgID = new Button();
        btnGetProgID.Location = new Point(0, 0);
        btnGetProgID.Size = new System.Drawing.Size(80,25);
        btnGetProgID.Text = "Get ProgIDs";
        btnGetProgID.Click += (sender,e)=>{ReloadProgIDs();};
        spl.Panel1.Controls.Add(btnGetProgID);
        
        txtFilter = new TextBox();
        txtFilter.Location = new Point(btnGetProgID.Right+10, 0);
        txtFilter.Size = new System.Drawing.Size(90,25);
        txtFilter.Text = "";
        txtFilter.TextChanged += (sender,e)=>{UpdateLsvProgID();}; // 入力中も変化するので鬱陶しいが妥協する。
        spl.Panel1.Controls.Add(txtFilter);
        new ToolTip().SetToolTip(txtFilter, "Filter text for <ProgID>, <Value of CLSID>\r\nTo show all, clear the TextBox content.");

        lsvProgID = new ListView();
        lsvProgID.Location = new Point(0, 30);
        lsvProgID.View = View.Details;
        lsvProgID.FullRowSelect =true;
        lsvProgID.GridLines = true;
        lsvProgID.Columns.Add("ProgID", 210, HorizontalAlignment.Left);
        lsvProgID.Columns.Add("CLSID", 210, HorizontalAlignment.Left);
        lsvProgID.Columns.Add("Value of CLSID", 210, HorizontalAlignment.Left);
        lsvProgID.SelectedIndexChanged += LsvProgID_SelectedIndexChanged;
        lsvProgID.ColumnClick += LsvProgID_ColumnClick;
        lsvProgID.ListViewItemSorter = new ListViewItemComparer(0); // index 0 のcolumnでソート設定しておく
        spl.Panel1.Controls.Add(lsvProgID);
        ////// 左 Panel ここまで


        ////// 右 Panel ここから
        txtProgID = new TextBox();
        txtProgID.Location = new Point(0, 0);
        txtProgID.Width = 250;
        txtProgID.Text = "InternetExplorer.Application";
        spl.Panel2.Controls.Add(txtProgID);
        new ToolTip().SetToolTip(txtProgID, "ProgID");

        btnGetMembers = new Button();
        btnGetMembers.Location = new Point(txtProgID.Right+10, 0);
        btnGetMembers.Size = new System.Drawing.Size(100,25);
        btnGetMembers.Text = "Get Members";
        btnGetMembers.Click += (sender,e)=>{ReloadLsvMemb(txtProgID.Text);};
        spl.Panel2.Controls.Add(btnGetMembers);
        new ToolTip().SetToolTip(btnGetMembers, "Get Members from ProgID in the TextBox");
        
        lsvMemb = new ListView();
        lsvMemb.Location = new Point(0, 30);
        lsvMemb.View = View.Details;
        lsvMemb.FullRowSelect =true;
        lsvMemb.GridLines = true;
        lsvMemb.Columns.Add("MemberType", 70, HorizontalAlignment.Left);
        lsvMemb.Columns.Add("ReturnType", 100, HorizontalAlignment.Left);
        lsvMemb.Columns.Add("MemberName", 130, HorizontalAlignment.Left);
        lsvMemb.Columns.Add("Definition", 350, HorizontalAlignment.Left);
        spl.Panel2.Controls.Add(lsvMemb);
        
        {
            var a = new ContextMenuStrip();
            a.Items.Add(new ToolStripMenuItem("Copy ReturnType", null, GetSelectedItems, "CopyReturnType"));
            a.Items.Add(new ToolStripMenuItem("Copy MemberName", null, GetSelectedItems, "CopyMemberName"));
            a.Items.Add(new ToolStripMenuItem("Copy Definition", null, GetSelectedItems, "CopyDefinition"));
            
            a.Opening += LsvMemb_ContextMenuStrip_Opening;
            lsvMemb.ContextMenuStrip = a;
        }
        ////// 右 Panel ここまで

        ClientSize = new System.Drawing.Size(800,530);
        
        Load      += Form_Resize;
        Resize    += Form_Resize;
        ResizeEnd += Form_Resize;
        spl.SplitterMoving += Form_Resize;
        spl.SplitterMoved  += Form_Resize;
    }
    
    void ClipboardSetText(string s)
    {
        if ( s==null || s=="" ){return;}
        
        try {
            Clipboard.SetText(s);
        }
        catch ( ExternalException ) { // クリアに失敗
            // 1回だけリトライする
            try {
                Clipboard.SetText(s);
            }
            catch ( ExternalException e2 ) {
                Console.WriteLine(e2);
            }
        }
    }
    
    
    void LsvMemb_ContextMenuStrip_Opening(object sender, CancelEventArgs e)
    {
        // https://qiita.com/Toraja/items/51ffc5cbfa0b9f8e154f
        Point p = lsvMemb.PointToClient(Cursor.Position);
        ListViewItem item = lsvMemb.HitTest(p).Item;
        
        contextMenuInfo = null;
        
        if (item == null) {
            e.Cancel = true;
        }
        else if ( item.Bounds.Contains(p) ) {
            contextMenuInfo = new MyListViewContextMenuInfo(lsvMemb, item);
        }
        else {
            e.Cancel = true;
        }
    }

    // ToolStripMenuItem.Click イベント
    void GetSelectedItems(object sender, EventArgs e)
    {
        var mi = (ToolStripMenuItem)sender;
        
        if ( contextMenuInfo != null ) {
            var memb = (ComObjectMember)contextMenuInfo.SelectedItem.Tag;
            
            string text = "";
            if ( mi.Name == "CopyDefinition" ) {
                text = memb.DefinitionText;
            }
            else if ( mi.Name == "CopyReturnType" ) {
                text = memb.ReturnTypeText;
            }
            else if ( mi.Name == "CopyMemberName" ) {
                text = memb.MemberName;
            }
            ClipboardSetText(text);
        }
    }

    void Form_Resize(object sender, EventArgs e)
    {
        lsvProgID.Size = new System.Drawing.Size(spl.Panel1.ClientSize.Width, spl.Panel1.ClientSize.Height - lsvProgID.Top);
        lsvMemb.Size   = new System.Drawing.Size(spl.Panel2.ClientSize.Width, spl.Panel2.ClientSize.Height - lsvMemb.Top);
    }

    // 再取得処理
    void ReloadProgIDs()
    {
        btnGetProgID.Enabled = false; // 処理に時間がかかるので操作を禁止しておく。※結局押したら意味ないかも
        try {
            allProgIDs = ProgIdInfo.GetProgIDs();
            UpdateLsvProgID();
        }
        finally {
            btnGetProgID.Enabled = true;
        }
    }
    
    // 再取得 or フィルタ変更時の lsvProgID 更新
    void UpdateLsvProgID()
    {
        lsvProgID.Items.Clear();
        lsvProgID.BeginUpdate();
        try {
            foreach ( var t in allProgIDs ) {
                string filterText = txtFilter.Text.Trim().ToLowerInvariant();
                
                if ( filterText == "" ||
                     t.ProgID.ToLowerInvariant().Contains(filterText) ||
                     t.ValueOfCLSID.ToLowerInvariant().Contains(filterText) ) {
                    lsvProgID.Items.Add(MyNewListItem_LsvProgID(t));
                }
            }
        }
        finally {
            lsvProgID.EndUpdate();
        }
    }

    void ReloadLsvMemb(string comName)
    {
        List<ComObjectMember> comObjMembers;

        btnGetMembers.Enabled = false;
        try {
            comObjMembers = ComObjectMember.GetMembers(comName);
            
            lsvMemb.Items.Clear();
            lsvMemb.BeginUpdate();
            try {
                foreach ( var t in comObjMembers ) {
                    lsvMemb.Items.Add(MyNewListItem_LsvMemb(t));
                }
            }
            finally {
                lsvMemb.EndUpdate();
            }
        }
        finally {
            btnGetMembers.Enabled = true;
        }
    }
    
    ListViewItem MyNewListItem_LsvProgID(ProgIdInfo progId)
    {
        var itm = new ListViewItem(new string[]{progId.ProgID, progId.CLSID, progId.ValueOfCLSID});
        itm.Tag = progId;
        return itm;
    }
    

    ListViewItem MyNewListItem_LsvMemb(ComObjectMember memb)
    {
        var itm = new ListViewItem(new string[]{memb.MemberTypeText, memb.ReturnTypeText, memb.MemberName, memb.DefinitionText});
        itm.Tag = memb;
        return itm;
    }
    
    
    void LsvProgID_SelectedIndexChanged(object sender, EventArgs e)
    {
        if (lsvProgID.SelectedIndices.Count != 1) {return;}
        int index = lsvProgID.SelectedIndices[0];
        ProgIdInfo progIdInfo = (ProgIdInfo)(lsvProgID.Items[index].Tag);
        txtProgID.Text = progIdInfo.ProgID;
    }
    
    void LsvProgID_ColumnClick(object sender, ColumnClickEventArgs e)
    {
        if ( e.Column >= 0 && e.Column < lsvProgID.Columns.Count ) {
            lsvProgID.ListViewItemSorter = new ListViewItemComparer(e.Column);
        }
    }

    public class ListViewItemComparer : IComparer
    {
        private int _column;
        public ListViewItemComparer(int col) { _column = col; }

        public int Compare(object obj1, object obj2)
        {
            string s1 = ((ListViewItem)obj1).SubItems[_column].Text;
            string s2 = ((ListViewItem)obj2).SubItems[_column].Text;
            return string.Compare(s1, s2, true); // 第3引数の true は、大文字小文字の差異を無視する指定(ignore case)
        }
    }
    
    
    class MyListViewContextMenuInfo
    {
        private ListViewItem _SelectedItem;
        private ListView     _Sender;
        
        public ListViewItem SelectedItem{get{return _SelectedItem;}}
        public ListView Sender{get{return _Sender;}}
        
        public MyListViewContextMenuInfo(ListView sender, ListViewItem selectedItem)
        {
            _Sender = sender;
            _SelectedItem = selectedItem;
        }
    }
    
    
    [STAThread]
    static void Main(){
        Application.Run(new MainForm());
    }
}

```


# 機能追加
・テキストコピーできるようにする 
→  済み(右側のみ) 右クリックで選択項目コピー可能
・ListView上の検索(filtering) 
→ 済み


# コンパイル方法

```
csc ^
/r:C:\Windows\assembly\GAC_MSIL\System.Management.Automation\1.0.0.0__31bf3856ad364e35\System.Management.Automation.dll ^
xxxx.cs
```

# 参考サイト
- [C# 上で PowerShell スクリプトを実行 (++C++; //未確認飛行 C)](https://ufcpp.net/study/powershell/interop.html)

- [PSObjectクラス (Microsoft) ](https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.psobject?view=pscore-6.2.0)

- [ListView(Details)のカラムヘッダ上でコンテキストメニューを出したくないっ！](https://qiita.com/Toraja/items/51ffc5cbfa0b9f8e154f)