none
エクスプローラでフォルダ選択時のイベント取得方法

    質問

  • こんにちは。

    Windows標準のエクスプローラでフォルダを選択した時にイベントを拾い、
    そして選択されたフォルダのパスを取得する方法で悩んでおります。

    環境は、
    OS:WindowsXP または Windows7
    開発環境:Visual Studio 2008(.Net Framework3.5)
    開発言語:C#
    になります。


    SystemFileWatcherでフォルダのラストアクセス日が変更になった場合は、
    イベント取得はできるのですが、当然同じ日だと取得はできません。

    いろいろサイト等で調べていると SHDocVWやShellFolderViewなどが
    出てきますが、実装方法が今ひとつわからない状態です。

    ご存知の方おられましたらご教授願います。
    よろしくお願い致します。

    2013年6月4日 13:31

回答

  • C# 系の資料が少ないので、以下、レガシーVBのコードですが。

    VB.NET 版のものがありました。
    http://bbs.wankuma.com/index.cgi?mode=al2&namber=55470&KLOG=93

    C# 版は……見当たらなかったので、書いてみました。
    Win8 + C# 2012 および Win 7 + C# 2008 で動作確認。

    using System.Runtime.InteropServices;
    using System.Reflection;
    using Shell32;  // 参照設定: Microsoft Shell Controls And Automation
    using SHDocVw;  // 参照設定: Microsoft Internet Controls
    
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
    
        private ShellBrowserWindow explorer = null;
        private ShellFolderView folderView = null;
    
        /// <summary>
        /// フォルダーの内容が選択されたときのイベント
        /// </summary>
        private void folderView_SelectionChanged()
        {
            List<string> items = new List<string>();
    
            FolderItem folderItem;
            folderItem = folderView.FocusedItem;
            items.Add("フォーカス: " + folderItem.Path);
            Marshal.ReleaseComObject(folderItem);
    
            FolderItems folderItems = folderView.SelectedItems();
            items.Add("選択数: " + folderItems.Count);
            for (int i = 0; i < folderItems.Count; i++)
            {
                folderItem = folderItems.Item(i);
                items.Add(i + ": " + folderItem.Path);
                Marshal.ReleaseComObject(folderItem);
            }
            Marshal.ReleaseComObject(folderItems);
    
            FillSelectionList(items);
        }
    
        /// <summary>
        /// フォルダ移動時に ShellFolderView を再取得する
        /// </summary>
        private void explorer_NavigateComplete2(object pDisp, ref object URL)
        {
            if (folderView != null)
            {
                folderView.SelectionChanged -= folderView_SelectionChanged;
                Marshal.ReleaseComObject(folderView);
            }
            object document = pDisp.GetType().InvokeMember("Document", BindingFlags.GetProperty, null, pDisp, null);
            folderView = (ShellFolderView)document;
            folderView.SelectionChanged += folderView_SelectionChanged;
    
            // タイトルバーに現在のフォルダを表示
            string title;
            Folder folder = folderView.Folder;
            title = "現在のフォルダ: " + folder.Title;
            Marshal.ReleaseComObject(folder);
            Invoke(new Action<string>(delegate(string s) {
                listBox1.Items.Clear();
                Text = s;
            } ), title);
        }
    
        private void Form1_Shown(object sender, EventArgs e)
        {
            // 操作用の Explorer を起動
            Type t = Type.GetTypeFromCLSID(new Guid("C08AFD90-F2A1-11D1-8455-00A0C91F3880"));
            object o = Activator.CreateInstance(t);
            explorer = (ShellBrowserWindow)o;
            explorer.NavigateComplete2 += explorer_NavigateComplete2;
            // explorer.OnQuit += explorer_OnQuit;
            explorer.Left = this.Left + 50;
            explorer.Top = this.Top + 50;
            object url = "C:\\";    // 初期表示フォルダ
            object flags = Type.Missing;
            object targetFrameName = Type.Missing;
            object postData = Type.Missing;
            object headers = Type.Missing;
            explorer.Navigate2(ref url, ref flags, ref targetFrameName, ref postData, ref headers);
            explorer.Visible = true;
        }
    
        /// <summary>
        /// 選択された内容を ListBox に表示。Explorer は外部プロセスなので、
        /// コントロール操作前に InvokeRequired をチェックする。
        /// </summary>
        /// <param name="items" />
        private void FillSelectionList(IEnumerable<string> items)
        {
            if (InvokeRequired)
            {
                Invoke(new Action<IEnumerable<string>>(FillSelectionList), items);
            }
            else
            {
                listBox1.BeginUpdate();
                listBox1.Items.Clear();
                foreach (string item in items)
                {
                    listBox1.Items.Add(item);
                }
                listBox1.EndUpdate();
            }
        }
    
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            //if (explorer != null)
            //{
            //    explorer.Quit();
            //}
        }
    }

    • 回答としてマーク sky1973 2013年6月5日 9:23
    • 編集済み 魔界の仮面弁士MVP 2013年6月5日 9:39 前回の修正ミスでIEnumerableがタグと誤解されて小文字化していたたため再修正
    2013年6月4日 17:08
  • フォルダー変更時には、NavigateComplete2 イベントが発生するかと思います。
    (Form のタイトルバーを変更している箇所です)

    現在のコードでは、そのイベント中にて

    string title;
    Folder folder = folderView.Folder;
    title = "現在のフォルダ: " + folder.Title;
    Marshal.ReleaseComObject(folder);

    としていますが、これは表示名であってパスではありません。

    より具体的なパスとして取得したいのであれば、上記の箇所を

    string folderPath;
    Folder2 folder =(Folder2) folderView.Folder;
    FolderItem fItem = folder.Self;
    folderPath = fItem.Path;
    Marshal.ReleaseComObject(fItem);
    Marshal.ReleaseComObject(folder);

    もしくは

    string folderPath;
    Folder folder = folderView.Folder;
    FolderItems fItems = folder.Items();
    FolderItem fItem = fItems.Item(Type.Missing);
    folderPath = fItem.Path;
    Marshal.ReleaseComObject(fItem);
    Marshal.ReleaseComObject(fItems);
    Marshal.ReleaseComObject(folder);

    などとしてみてください。

    • 回答としてマーク sky1973 2013年6月5日 9:23
    2013年6月5日 4:15

すべての返信

  • そうですね、 ShellFolderView オブジェクトから辿ることになると思います。

    C# 系の資料が少ないので、以下、レガシーVBのコードですが。

    http://hpcgi1.nifty.com/MADIA/VBBBS/wwwlng.cgi?print+200603/06030011.txt
    http://hpcgi1.nifty.com/MADIA/VBBBS/wwwlng.cgi?print+200406/04060065.txt
    http://msdn.microsoft.com/en-us/library/windows/desktop/bb774049.aspx

    肝心のイベントとしては、 SelectionChanged イベントが使えます。

    ただし、「どのエクスプローラーを対象とするのか」が問題となります。既に複数の Explorer が起動していた場合はどうするのか。それとも、起動済みのものを拾うのではなく、C# から新たに Explorer を起動し、それのみを対象とするのか。

    新たに起動した物のみを対象とするのであれば話は比較的単純ですが、もしも起動済みの物を拾うのであれば、ShellWindows オブジェクトを列挙して、その中から対象としたい Explorer を判断して拾い上げるような処置が必要ですね。

    2013年6月4日 15:40
  • C# 系の資料が少ないので、以下、レガシーVBのコードですが。

    VB.NET 版のものがありました。
    http://bbs.wankuma.com/index.cgi?mode=al2&namber=55470&KLOG=93

    C# 版は……見当たらなかったので、書いてみました。
    Win8 + C# 2012 および Win 7 + C# 2008 で動作確認。

    using System.Runtime.InteropServices;
    using System.Reflection;
    using Shell32;  // 参照設定: Microsoft Shell Controls And Automation
    using SHDocVw;  // 参照設定: Microsoft Internet Controls
    
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
    
        private ShellBrowserWindow explorer = null;
        private ShellFolderView folderView = null;
    
        /// <summary>
        /// フォルダーの内容が選択されたときのイベント
        /// </summary>
        private void folderView_SelectionChanged()
        {
            List<string> items = new List<string>();
    
            FolderItem folderItem;
            folderItem = folderView.FocusedItem;
            items.Add("フォーカス: " + folderItem.Path);
            Marshal.ReleaseComObject(folderItem);
    
            FolderItems folderItems = folderView.SelectedItems();
            items.Add("選択数: " + folderItems.Count);
            for (int i = 0; i < folderItems.Count; i++)
            {
                folderItem = folderItems.Item(i);
                items.Add(i + ": " + folderItem.Path);
                Marshal.ReleaseComObject(folderItem);
            }
            Marshal.ReleaseComObject(folderItems);
    
            FillSelectionList(items);
        }
    
        /// <summary>
        /// フォルダ移動時に ShellFolderView を再取得する
        /// </summary>
        private void explorer_NavigateComplete2(object pDisp, ref object URL)
        {
            if (folderView != null)
            {
                folderView.SelectionChanged -= folderView_SelectionChanged;
                Marshal.ReleaseComObject(folderView);
            }
            object document = pDisp.GetType().InvokeMember("Document", BindingFlags.GetProperty, null, pDisp, null);
            folderView = (ShellFolderView)document;
            folderView.SelectionChanged += folderView_SelectionChanged;
    
            // タイトルバーに現在のフォルダを表示
            string title;
            Folder folder = folderView.Folder;
            title = "現在のフォルダ: " + folder.Title;
            Marshal.ReleaseComObject(folder);
            Invoke(new Action<string>(delegate(string s) {
                listBox1.Items.Clear();
                Text = s;
            } ), title);
        }
    
        private void Form1_Shown(object sender, EventArgs e)
        {
            // 操作用の Explorer を起動
            Type t = Type.GetTypeFromCLSID(new Guid("C08AFD90-F2A1-11D1-8455-00A0C91F3880"));
            object o = Activator.CreateInstance(t);
            explorer = (ShellBrowserWindow)o;
            explorer.NavigateComplete2 += explorer_NavigateComplete2;
            // explorer.OnQuit += explorer_OnQuit;
            explorer.Left = this.Left + 50;
            explorer.Top = this.Top + 50;
            object url = "C:\\";    // 初期表示フォルダ
            object flags = Type.Missing;
            object targetFrameName = Type.Missing;
            object postData = Type.Missing;
            object headers = Type.Missing;
            explorer.Navigate2(ref url, ref flags, ref targetFrameName, ref postData, ref headers);
            explorer.Visible = true;
        }
    
        /// <summary>
        /// 選択された内容を ListBox に表示。Explorer は外部プロセスなので、
        /// コントロール操作前に InvokeRequired をチェックする。
        /// </summary>
        /// <param name="items" />
        private void FillSelectionList(IEnumerable<string> items)
        {
            if (InvokeRequired)
            {
                Invoke(new Action<IEnumerable<string>>(FillSelectionList), items);
            }
            else
            {
                listBox1.BeginUpdate();
                listBox1.Items.Clear();
                foreach (string item in items)
                {
                    listBox1.Items.Add(item);
                }
                listBox1.EndUpdate();
            }
        }
    
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            //if (explorer != null)
            //{
            //    explorer.Quit();
            //}
        }
    }

    • 回答としてマーク sky1973 2013年6月5日 9:23
    • 編集済み 魔界の仮面弁士MVP 2013年6月5日 9:39 前回の修正ミスでIEnumerableがタグと誤解されて小文字化していたたため再修正
    2013年6月4日 17:08
  • 魔界の仮面弁士様

    早速の回答ありがとうございます。
    親切にソースまで記述して頂きまして大変感謝しております。

    回答内容について質問させてください。
    教えて頂きました「SelectionChanged」だとエクスプローラの左側(フォルダツリー)を
    選択した時にイベントが発生しませんでした。

    ユーザはフォルダツリーでフォルダ選択することが考えられますので、
    このフォルダツリー選択時にもイベント取得は可能なのでしょうか?

    ご教授お願い致します。



    • 編集済み sky1973 2013年6月5日 3:32
    2013年6月5日 3:31
  • フォルダー変更時には、NavigateComplete2 イベントが発生するかと思います。
    (Form のタイトルバーを変更している箇所です)

    現在のコードでは、そのイベント中にて

    string title;
    Folder folder = folderView.Folder;
    title = "現在のフォルダ: " + folder.Title;
    Marshal.ReleaseComObject(folder);

    としていますが、これは表示名であってパスではありません。

    より具体的なパスとして取得したいのであれば、上記の箇所を

    string folderPath;
    Folder2 folder =(Folder2) folderView.Folder;
    FolderItem fItem = folder.Self;
    folderPath = fItem.Path;
    Marshal.ReleaseComObject(fItem);
    Marshal.ReleaseComObject(folder);

    もしくは

    string folderPath;
    Folder folder = folderView.Folder;
    FolderItems fItems = folder.Items();
    FolderItem fItem = fItems.Item(Type.Missing);
    folderPath = fItem.Path;
    Marshal.ReleaseComObject(fItem);
    Marshal.ReleaseComObject(fItems);
    Marshal.ReleaseComObject(folder);

    などとしてみてください。

    • 回答としてマーク sky1973 2013年6月5日 9:23
    2013年6月5日 4:15
  • 魔界の仮面弁士様

    教えて頂いた内容で実現できました。
    本当に助かりました。ありがとうございました。


    2013年6月5日 9:23