[C#] Example of Explorer Operation Automation: Selecting and Launching Files with Reflection and Shell
枯れたおっさんはいつもC#しか書かない
既出な気がするけど、Shell使ったExplorer操作の例があまりないようなので。
この記事のCodeはAI6割、手打ちでの調整が4割ぐらいです(たぶん)
Linkは自分で普通に探してます(AIによるWebSearchも使用する)
既存のExplorerに対して確実に操作を行う方法が判明したので3ストック以上で公表します。
→公表済み
最近の流行っぽいので真似しましたが、AIの使用はもはや暗黙の了解だと考えております。リファレンスとして実際のアクセス可能なLinkを貼ればよいと思う。
参考記事
C#: 実行時に名前を指定してメソッドを呼び出す
https://qiita.com/nicklegr/items/296bf3533c592a5563c1
C# dynamic型の罠:マメにキャストして型を明示しないと・・・コンパイル時の型チェックが甘くなる&実行時の処理が増える
https://qiita.com/kob58im/items/6361556750fb0b575800
Developing with Windows Explorer
https://learn.microsoft.com/en-us/windows/win32/shell/developing-with-windows-explorer
エクスプローラ(Explorer.EXE)で開いているフォルダのパスを取得する
http://tinqwill.blog59.fc2.com/blog-entry-84.html
C++20&WinAPI &WIL Shell.Applicationでエクスプローラーウィンドウの情報を取得する
https://potisan-programming-memo.hatenablog.jp/entry/2022/04/24/102126
エクスプローラーで特定のフォルダを開くときはWinApiを使おう
https://zenn.dev/nabezokodaikon/articles/83701c6d29eb69
今回参考度が高い記事。
Shell.Open()だと新規Windowとして開いてしまうのでこちらも併用した。
前置き
普通のプロセス起動(Process.Start)だとExplorer.exeが何度も開いてしまい、メモリを圧迫します。これを回避するCode例です。
ファイルを開く、特定のフォルダを新規タブで開くなど、かなり色々やれます。無理だと思い込んでいた
型チェックが利かないので実行時エラーになりません
→ メソッド名が間違っててもわかりません
→ 引数が要るのか要らないのかすら一見して分かりません。
Win32APIも併用してます。
公開されたCOM Interfaceにより安定したExplorerの操作が実現されます。
内部APIが非公開のプロセスを操作する場合
→ APIやCOM Interfaceが非公開だとリバースエンジニアリングで解析して無理やり呼ぶしかなく、不安定。
 → その場合でもUIAutomationという手段が用意されている
Explorerは公開APIがあるので可能なんです。
→ Shell.Openで開いたexeは重複チェックができない 
 ┗ Explorer.exeと同様の公開Com インターフェースがあれば可能。
  ┗ 自分のウィンドウやドキュメントを列挙する仕組みを公開していれば、Explorer と同じように「既存インスタンスを探してアクティブ化」できる
$\color{lightblue}{\tiny \textsf{電卓を操作するだけの記事とかつまらない}}$
想定以上に反響があったので(この前寄付を貰ったので)
Gitを置いておきます。
ありがとうございました。
Explorer.exeを開い
て特定のファイルを選択状態にする場合 その1
そんな簡単に出てこないネタなんだが勿体ない
確実に既存のExplorer.exeを列挙するCodeです。
 →その2だとなぜかユーザー操作のExplorer Windowが取得できない
他言語の情報だろうと役に立つことはよくあります。
詳細な説明
こんな記事もあるが
C#でIEを操作する
→ 仕組みとしては「ShellWindows = Explorer + ブラウザ(かつては IE)」という仕様
なので実はExplorer(ファイラー)も操作できる。
流石にそんなこと誰も知らないだろう。
→ 今ではNavigateメソッドが関連付けされたブラウザで開く仕様になってる
昔はInternet Explorere(IE)が存在したのでexplorer.exeと混ざる可能性があったが、今(Windows11)では存在していないので区別しなくていい
実行結果 その1
動画では1秒待機にしているので見かけ上もっさり気味。
50msでぬる挙動。
必要なもの
SHDocVw (Microsoft Internet Controls)  を参照追加
Microsoft Internet Controls → これが SHDocVw 名前空間になる
(InternetExplorer / IWebBrowser2 などが入っている)
Code
新規で開く場合にファイル選択状態にするため、
Process.Start("explorer.exe", $"/select , \"{_filePath}\"");も併用している
方法2と同様Com Interfaceを介して呼んでいるのでDynamic(リフレクション)的な取り扱いが可能なうえに、使えるメソッドが大幅に増える。
便宜上、一部抜粋するがこれで動くだろう。
using Microsoft.Win32;
using SHDocVw;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
public partial class MainWindow : Window
   {
     public MainWindow()
       {
         InitializeComponent();
       }
       
       [DllImport("user32.dll")]
       [return: MarshalAs(UnmanagedType.Bool)]
       private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
       [DllImport("user32.dll")]
    private static extern bool SetForegroundWindow(IntPtr hWnd);
       string _filePath = string.Empty;
        private void testNutton_Click(object sender, RoutedEventArgs e)
        {
            // ② Shell COM を列挙して HWND と一致するものを探す
            Type? shellType = Type.GetTypeFromProgID("Shell.Application");
            if (shellType is null)
                throw new InvalidOperationException("Shell.Application COM が利用できません。");
            dynamic? dynamicShell = Activator.CreateInstance(shellType);
            if (dynamicShell == null)
            {
                throw new InvalidOperationException("");
            }
            ShellWindows windows = dynamicShell.Windows();
            string? fileName = Path.GetFileName(_filePath);
            string? folderName = Path.GetDirectoryName(_filePath);
            try
            {
            //InternetExplorer を列挙
+            foreach (InternetExplorer window in windows)
           列挙
                //Explorerを開く(Navigate1)はブラウザを開く
                window.Navigate2(folderName);
   ///////System.Runtime.InteropServices.COMException: 'Unexpected HRESULT has been returned from a call to a COM component.' 対策
+   if (window.ReadyState != SHDocVw.tagREADYSTATE.READYSTATE_COMPLETE)
+       Thread.Sleep(50); //Load完了していない場合に50ms待機
                foreach (var item in window.Document.Folder.Items())
                    if (string.Equals(item.Name, fileName, StringComparison.OrdinalIgnoreCase))
                    {
+                        window.Document.SelectItem(item, 0x4);   // 既存解除
                        window.Document.SelectItem(item, 1);   // 選択
                    }
                //Activae Window
                ShowWindow((nint)window.HWND, 9);
                //private const int SW_RESTORE = 9; // 最小化から復元
                SetForegroundWindow((nint)window.HWND);
                return;
            }
          //Explorer Window not found
            //dynamicShell.Explore(folderName);
            Process.Start("explorer.exe", $"/select , \"{_filePath}\"");
        }
        catch (Exception ex)
        {
        MessageBox.Show(ex.Message);
        }
ファイルパス取得用(ファイル選択に使う)
 private void reffernceFolderButton_Click(object sender, RoutedEventArgs e)
        {
            // ダイアログのインスタンスを生成
            var dialog = new OpenFileDialog();
            // ファイルの種類を設定
            dialog.Filter = "全てのファイル (*.*)|*.*";
            // ダイアログを表示する
            if (dialog.ShowDialog() == true)
            {
                // 選択されたファイル名 (ファイルパス) をメッセージボックスに表示
                testBox.Text = dialog.FileName;
                _filePah = dialog.FileName;
            }
        }
XAML
Grid周りだけおいとくので、新規プロジェクトで置き換えて
クソレガシーWinformより便利だと実感すべし
 <Grid>
     <StackPanel Orientation="Vertical" VerticalAlignment="Center">
     <StackPanel Orientation="Horizontal" 
     HorizontalAlignment="Center">
         
             <TextBox x:Name="testBox" Width="200" Height="50"
     TextWrapping="NoWrap"
     Margin="25"/>
             <Button x:Name="reffernceFolderButton" Width="80" Height="50"  FontSize="30"
         Click="reffernceFolderButton_Click"
         Content="file"/>
             <Button Content="OpenExplorer" x:Name="OpenExplorer" Height="50"  Width="130"
            Click="OpenExplorer_Click"/>
         
     </StackPanel>
         <Button x:Name="testNutton" Content="test" Width="50" Height="50"
         Click="testNutton_Click"/>
     </StackPanel>
 </Grid>
あとがき
こんな面倒な手間をかけさせてくれてどうもありがとう(もう少しストックに時間掛かると思っていた ^_^###)
みなさん結構知りたがりですね。
Explorer.exeを開いて特定のファイルを選択状態にする場合 その2
実行結果(Twitter)
(ユーザー操作のものは列挙されず、2つ目が開いてしまう)
Code
以下のCodeだと既に開いている(ユーザー操作の)Explorer.exeがあるとチェックされず、追加でWindowが開く。
一応重複チェックは出来てるのでSelectFileInExplorerメソッドは要らないかも。一応実験目的で追加した。
→ (プログラム外で)既に開いているExplorer.exeがあっても再利用されず、必ず新規のExplorerから重複チェックが行われます。
Windows11の場合の解決方法がちょっと不明です。
拘る必要はないと思われるが
→ 既に方法が判明しており、ユーザー操作ののExplorer.exeに対して開く事が出来るようになりました。
using Microsoft.Win32;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
namespace ExplorerReflectionTest
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        [DllImport("user32.dll")]
        private static extern bool SetForegroundWindow(IntPtr hWnd);
        [DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
        public static extern int SHParseDisplayName(
    [MarshalAs(UnmanagedType.LPWStr)] string pszName,
    IntPtr pbc,
    out IntPtr ppidl,
    uint sfgaoIn,
    out uint psfgaoOut);
        [DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
        public static extern int SHOpenFolderAndSelectItems(
    IntPtr pidlFolder,
    uint cidl,
    [MarshalAs(UnmanagedType.LPArray)] IntPtr[] apidl,
    uint dwFlags);
        public static void ActivateExplorerWindow(dynamic explorer)
        {
            try
            {
                // explorer.HWND でウィンドウハンドルを取得
                IntPtr hwnd = (IntPtr)explorer.HWND;
                if (hwnd == IntPtr.Zero)
                    return;
                SetForegroundWindow(hwnd); // 前面に表示
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"Explorer をアクティブにできません: {ex.Message}");
            }
        }
        string _filePah = string.Empty;
        private void reffernceFolderButton_Click(object sender, RoutedEventArgs e)
        {
            // ダイアログのインスタンスを生成
            var dialog = new OpenFileDialog();
            // ファイルの種類を設定
            dialog.Filter = "全てのファイル (*.*)|*.*";
            // ダイアログを表示する
            if (dialog.ShowDialog() == true)
            {
                // 選択されたファイル名 (ファイルパス) をメッセージボックスに表示
                testBox.Text = dialog.FileName;
                _filePah = dialog.FileName;
            }
        }
        public static void SelectFileInExplorer(string filePath)
        {
            IntPtr pidlFolder, pidlFile;
            uint attrs;
            string? folder = Path.GetDirectoryName(filePath);
            // フォルダ PIDL
            SHParseDisplayName(folder, IntPtr.Zero, out pidlFolder, 0, out attrs);
            // ファイル PIDL
            SHParseDisplayName(filePath, IntPtr.Zero, out pidlFile, 0, out attrs);
            try
            {
                SHOpenFolderAndSelectItems(pidlFolder, 1, new[] { pidlFile }, 0);
            }
            finally
            {
                Marshal.FreeCoTaskMem(pidlFolder);
                Marshal.FreeCoTaskMem(pidlFile);
            }
        }
        private void OpenExplorer_Click(object sender, RoutedEventArgs e)
        {
            string filePath = _filePah;
            if (!File.Exists(filePath))
                return;
            string? folderName = Path.GetDirectoryName(filePath);
            string fileName = Path.GetFileName(filePath);
            dynamic? shell = null;
            try
            {
                Type? shellType = Type.GetTypeFromProgID("Shell.Application");
                if (shellType is null)
                    return;
                shell = Activator.CreateInstance(shellType)!;
                //COM のインスタンスなので、使い終わったら必ず ReleaseComObject。
                
                
                dynamic windows = shell.Windows();
                dynamic? explorer = null;
                foreach (dynamic window in windows)
                {
                    string? appPath = window.FullName as string;
                    if (string.IsNullOrEmpty(appPath))
                        continue;
                    // explorer.exe 以外はスキップ
                    if (!appPath.EndsWith("explorer.exe", StringComparison.OrdinalIgnoreCase))
                        continue;
                    string path = Path.GetFullPath(window.Document.Folder.Self.Path);
                    if (string.Equals(path.TrimEnd('\\'), folderName, StringComparison.OrdinalIgnoreCase))
                    {
                        explorer = window;
                        break;
                    }
                }
                if (explorer != null)
                {
                    dynamic items = explorer.Document.Folder.Items();
                    foreach (var item in items)
                    {
                        if (string.Equals(item.Name, fileName, StringComparison.OrdinalIgnoreCase))
                        {
                            explorer.Document.SelectItem(item,0x4); // Erase Selection
                            explorer.Document.SelectItem(item, 0x1); // select
                            break;
                        }
                    }
                    ActivateExplorerWindow(explorer);
                    shell.Open(filePath);
                    //File Open(関連付けられたexeで開く)
                }
                else
                {
                    SelectFileInExplorer(filePath);
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
            finally
            {
                if (shell != null)
                    Marshal.ReleaseComObject(shell);
            }
        }
    }
}
注意書きとフラグのパラグラフ
ILFreee関数 (shlobj_core.h)というのがあるが、Win95/98/Meのレガシー環境向けなので使用しないこと.
https://stackoverflow.com/questions/63736090/should-i-cotaskmemfree
explorer.Document.SelectItem(item, 0x1)の第2引数について
Windows Shell API における「アイテム選択フラグ (SVSIF_xxx)」 の一つです。
_SVSIF列挙体 _SVSIF enumeration (shobjidl_core.h)
以下表(翻訳済み)
| 定数 | 値 | 16進数 | 説明 | 
|---|---|---|---|
| SVSI_DESELECT | 0 | 0x00000000 | 項目の選択を解除します。 | 
| SVSI_SELECT | 0x1 | 0x00000001 | 項目を選択します。 | 
| SVSI_EDIT | 0x3 | 0x00000003 | アイテムの名前を名前変更モードにします。この値にはSVSI_SELECTが含まれます。 | 
| SVSI_DESELECTOTHERS | 0x4 | 0x00000004 | 選択されている項目以外のすべての項目を選択解除します。項目パラメータがNULLの場合、すべての項目の選択を解除します。 | 
| SVSI_ENSUREVISIBLE | 0x8 | 0x00000008 | 1画面にすべての内容を表示できないフォルダーの場合は、選択した項目を含む部分を表示します。 | 
| SVSI_FOCUSED | 0x10 | 0x00000010 | 複数の項目が選択されている場合は、選択された項目にフォーカスを与え、メソッドによって返されるコレクションのリストの先頭に項目を配置します。 | 
| SVSI_TRANSLATEPT | 0x20 | 0x00000020 | 入力ポイントを画面座標からリストビュークライアント座標に変換します。 | 
| SVSI_SELECTIONMARK | 0x40 | 0x00000040 | IFolderView::GetSelectionMarkedItemを使用してクエリできるように項目をマークします。 | 
| SVSI_POSITIONITEM | 0x80 | 0x00000080 | ウィンドウのデフォルトビューを使用して項目を配置します。ほとんどの場合、項目は最初に利用可能な位置に配置されます。ただし、マウスで位置付けされたコンテキストメニューの処理中に呼び出しが行われた場合は、コンテキストメニューの位置に基づいて項目が配置されます。 | 
| SVSI_CHECK | 0x100 | 0x00000100 | アイテムはチェックされている必要があります。このフラグは、チェックモードがサポートされているビュー内のアイテムで使用されます。 | 
| SVSI_CHECK2 | 0x200 | 0x00000200 | ビューがトライチェックモードの場合の2番目のチェック状態。チェック状態には3つの値があります。トライチェックモードを指定するには、IFolderView2::SetCurrentFolderFlagsで FWF_TRICHECKSELECT を指定します。FWF_TRICHECKSELECT の3つの状態は、チェックなし、SVSI_CHECK、SVSI_CHECK2 です。 | 
| SVSI_KEYBOARDSELECT | 0x401 | 0x00000401 | 項目を選択し、キーボードで選択されたものとしてマークします。この値にはSVSI_SELECTが含まれます。 | 
| SVSI_NOTAKEFOCUS | 0x40000000 | 0x40000000 | 項目を選択またはフォーカスする操作では、ビュー自体にフォーカスを設定しないでください。 | 
FreeCoTaskMemとMarshal.ReleaseComObjectの違い
① Marshal.FreeCoTaskMem
対象: Marshal.AllocCoTaskMem などで 自分で確保したアンマネージメモリ領域
役割: 確保した生のポインタを OS (CoTaskMemAlloc で割り当てられたメモリ) に返す。
https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.freecotaskmem?view=net-9.0
②Marshal.ReleaseComObject
対象: Activator.CreateInstance や Shell.Application で取得した COM オブジェクト
役割: .NET ランタイムが保持している COM オブジェクト参照のカウント(AddRef/Release)を減らす。
まとめ
- 
Marshal.FreeCoTaskMem 
 → 「生メモリ(IntPtr)を解放」する。
- 
Marshal.ReleaseComObject 
 → 「COM オブジェクトの参照カウントを減らして、最終的に解放」する。
Shell.Openと似たようなメソッド(Com interface経由)の説明
| メソッド名 | 説明 | リファレンス | 
|---|---|---|
| Open | 指定されたフォルダパスをエクスプローラーウィンドウで開く(新規ウィンドウとして開く)。ファイルパスを指定した場合、デフォルトの動作(関連付けられたアプリで開く)を実行します。 | https://learn.microsoft.com/en-us/windows/win32/shell/shell-open | 
| Explore | 指定されたフォルダを2ペインのエクスプローラビュー(ツリーとリスト)で開く。引数はフォルダパス(文字列またはShellSpecialFolderConstan | https://learn.microsoft.com/en-us/windows/win32/shell/shell-explore | 
| BrowseForFolder | ユーザーにフォルダ選択ダイアログを表示し、選択されたフォルダを返す。引数として親ウィンドウハンドルやタイトルを指定可能。 | https://learn.microsoft.com/en-us/windows/win32/shell/shell-browseforfolder | 
| ShellExecute (WScript.Shell経由) | ファイルやURLを指定し、関連付けられたアプリケーションで実行(起動)。引数にファイルパス、動詞(open/editなど)を指定。 | https://learn.microsoft.com/en-us/windows/win32/shell/shell-shellexecute | 
その他
| メソッド名 | 説明 | リファレンス | 
|---|---|---|
| FindFiles | 「ファイルの検索」ダイアログを表示(Startメニューの検索相当)。 | https://learn.microsoft.com/en-us/windows/win32/shell/shell-findfiles | 
| shell.Application | Shellオブジェクト自体を返す(メタプロパティ)。親オブジェクト参照に。 | https://learn.microsoft.com/en-us/windows/win32/shell/shell-appliction | 
| Shell.Windows | オープン中のShell関連ウィンドウ(Explorerなど)のコレクションを取得。ウィンドウ数や操作に便利。 | https://learn.microsoft.com/en-us/windows/win32/shell/shell-windows 本稿の既存Explorer検索に活用。 | 
あとがき
如何でしたか?(構文)
確かにいつもC#しかかかないがそれは時間がないからで
他の言語の記事はけっこう手当たり次第に読んでるつもりです。参考になることも多いです
C#自体がけっこう"器用"な言語なんで、だいたい取り入れることが出来る
高級言語だからどうしても実行速度とか、出来ないことが出てくるのが唯一の難点ですかねえ
お願い
GitHub Sponserdと連携しました!私も誰かに寄付しようと思います
良い記事を書き続けたいので寄付をお願いします。500円くらいで良いです。動物園通いをしたいの

リポジトリへのスターもお願いします。
彼女も紹介してください。
仕事を紹介してください。
海外旅行も行きたいです
....etc
