C#でエクスプローラのコンテキストメニューに項目を追加する

# はじめに

Windowsのエクスプローラでファイルを右クリックしたときに出てくるメニュー（コンテキストメニュー）に項目を追加する方法です。[SharpShell](https://github.com/dwmkerr/sharpshell)を使うほうが楽ですが、ちょっとしたメニューを追加する分にはこの方法で十分だと思います。以下の記事を参考にさせていただきました。  

[.NET で Windows エクスプローラのコンテキストメニューを拡張 #C# - Qiita](https://qiita.com/tinymouse/items/26c1bfa0d2a6a2116cff)  

![contextsample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3193449/1cbeacd1-1898-9877-a317-9e59cd101ede.png)  

# 作成方法

[ここの作成方法](https://qiita.com/nekotadon/items/f17f6762e4ea58ef1578#%E4%BD%9C%E6%88%90%E6%96%B9%E6%B3%95)と同じ方法を実施し、ビルドまで行います。但し、以下の変更をします。

* プロジェクト名は「MyAddin」ではなく「ContextMenuExtension」とします。  
* プロジェクトを作成したら、`System.Windows.Forms`を参照に追加します。「プロジェクト」から「参照の追加」を選択し、「System.Windows.Forms」にチェックを入れて「OK」を押します。
* 「コードを記載します。」の箇所のコードは[ベースコード](#ベースコード)を貼り付けます。  
* `//GUID（変更必要）`のコメント部は先頭の1箇所しかないため、この1箇所のみGUIDを新規作成し、置き換えます。  
* キーファイル名は「MyAddin」ではなく「ContextMenuExtension」とします。  
* 今回はExcelではなくWindowsが対象なので64bit Windowsであれば構成をx64に、32bit Windowsであれば構成をx86にします。  

# 登録と解除

**登録して使用する**  

管理者権限でコマンドプロンプトを開いてRegAsmを利用して登録します。登録するとファイルをクリックしたときに「ファイル名を表示(E)」メニューが追加されます。  
```bat
REM 以下は64bit版Windowsの場合。32bitの場合はFramework64をFrameworkに変更する。
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\RegAsm.exe /codebase ContextMenuExtension.dll
```

**解除して使用をやめる**  

管理者権限でコマンドプロンプトを開いてRegAsmを利用して登録を解除します。その後再起動をして`ContextMenuExtension.dll`を削除します。  

```bat
REM 以下は64bit版Windowsの場合。32bitの場合はFramework64をFrameworkに変更する。
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\RegAsm.exe /u ContextMenuExtension.dll
```

# カスタマイズ

[ベースコード](#ベースコード)内の`設定ここから`から`設定ここまで`までのソースコードを変更することでコンテキストメニューをカスタマイズできます。

**メニュー名の変更**  

以下の箇所を変更することで表示するメニュー名を変更できます。  

```cs
//メニュー名
private string MENU_NAME = "ファイル名を表示(&E)";
```

**メニュー選択時の動作の変更**  

以下の箇所を変更することでメニューを選択したときに行われる動作を変更できます。選択されたファイルは`string[] filelists`に格納されています。  

```cs
//メニューが選択されたときに行われる処理
private void ActionMethod()
{
    MessageBox.Show("選択されたファイル: " + Environment.NewLine + string.Join(Environment.NewLine, filelists));
}
```

**メニューを表示するファイルの変更**  

以下の箇所を変更することでメニューを表示するファイルを変更することができます。  

すべてのファイルにメニューを表示（ベースコードの場合）
```cs
//メニューを表示するかどうか
private bool IsShowMenu()//戻り値がtrueの場合メニューを表示
{
    return true;
}
```

例）拡張子がbmpのファイルにのみメニューを表示

```cs
//メニューを表示するかどうか
private bool IsShowMenu()//戻り値がtrueの場合メニューを表示
{
    //メニュー表示対象の拡張子
    string targetExtension = ".bmp";

    bool menuShow = true;

    if (filelists != null && filelists.Length > 0)
    {
        //選択されたファイルの拡張子が全てtargetExtensionかどうか
        foreach (string file in filelists)
        {
            if (Path.GetExtension(file).ToLower() != targetExtension.ToLower())
            {
                menuShow = false;
                break;
            }
        }
    }
    else
    {
        menuShow = false;
    }

    return menuShow;
}
```
例）ファイルサイズが1MB以上の場合のみメニューを表示
```cs
//メニューを表示するかどうか
private bool IsShowMenu()//戻り値がtrueの場合メニューを表示
{
    bool menuShow = true;

    if (filelists != null && filelists.Length > 0)
    {
        try
        {
            foreach (string file in filelists)
            {
                if ((new FileInfo(file)).Length < 1024 * 1024)
                {
                    menuShow = false;
                    break;
                }
            }
        }
        catch (Exception ex)
        {
            menuShow = false;
        }
    }
    else
    {
        menuShow = false;
    }

    return menuShow;
}
```

例）`C:\work\`配下のファイルにのみメニューを表示
```cs
//メニューを表示するかどうか
private bool IsShowMenu()//戻り値がtrueの場合メニューを表示
{
    bool menuShow = true;

    if (filelists != null && filelists.Length > 0)
    {
        try
        {
            foreach (string file in filelists)
            {
                if (!file.StartsWith(@"C:\work\"))
                {
                    menuShow = false;
                    break;
                }
            }
        }
        catch (Exception ex)
        {
            menuShow = false;
        }
    }
    else
    {
        menuShow = false;
    }

    return menuShow;
}
```

**フォルダの余白のコンテキストメニューに追加**  

以下の箇所を変更することでフォルダの余白のコンテキストメニューに対してメニューを追加することができます。余白を右クリックされたフォルダは`string folderpath`に格納されています。  

```cs
//レジストリの場所 HKEY_CLASSES_ROOT\
private const string TargetFileType = @"Directory\Background";
```

具体的なコード例です。`設定ここから`から`設定ここまで`までを以下に置き換えると`C:\work\`配下のフォルダの余白を右クリックしたときのみ「コマンドプロンプトを表示(E)」メニューを表示します。  

```cs
//--------------------------設定ここから--------------------------

//余白を右クリックされたフォルダは string folderpath に格納されている

//メニュー名
private string MENU_NAME = "コマンドプロンプトを表示(&E)";

//メニューが選択されたときに行われる処理
private void ActionMethod()
{
    System.Diagnostics.ProcessStartInfo processStartInfo = new System.Diagnostics.ProcessStartInfo();
    processStartInfo.FileName = "cmd.exe";
    processStartInfo.Arguments = "/s /k pushd " + "\"" + folderpath + "\"";
    System.Diagnostics.Process.Start(processStartInfo);
}

//メニューを表示するかどうか
private bool IsShowMenu()//戻り値がtrueの場合メニューを表示
{
    //C:\work配下の場合のみメニュー表示
    return folderpath != null && folderpath.StartsWith(@"C:\work");
}

//レジストリの場所 HKEY_CLASSES_ROOT\
private const string TargetFileType = @"Directory\Background";

//--------------------------設定ここまで--------------------------
```


# ベースコード
```cs:Class1.cs
using Microsoft.Win32;
using System;
using System.Collections.Specialized;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;

namespace ContextMenuExtension//プロジェクト名
{
    [ClassInterface(ClassInterfaceType.None)]
    [Guid("90703BC8-1487-4B27-AA96-043C54268C97")]//GUID（変更必要）
    [ComVisible(true)]
    public class ContextMenuExtension : IShellExtInit, IContextMenu
    {
        //--------------------------設定ここから--------------------------

        //選択されたファイル/フォルダは string[] filelists に格納されている

        //メニュー名
        private string MENU_NAME = "ファイル名を表示(&E)";

        //メニューが選択されたときに行われる処理
        private void ActionMethod()
        {
            MessageBox.Show("選択されたファイル: " + Environment.NewLine + string.Join(Environment.NewLine, filelists));
        }

        //メニューを表示するかどうか
        private bool IsShowMenu()//戻り値がtrueの場合メニューを表示
        {
            return true;
        }

        //レジストリの場所 HKEY_CLASSES_ROOT\
        private const string TargetFileType = "*";

        //--------------------------設定ここまで--------------------------

        //以下おまじない

        //格納用変数
        private string[] filelists;//選択されたファイル/フォルダ
        private string folderpath;//どのフォルダで右クリックされたか

        public void Initialize(IntPtr pidlFolder, IntPtr pDataObj, IntPtr hKeyProgID)
        {
            if (TargetFileType == "*")
            {
                // 選択されているファイル/ディレクトリを取得
                if (pDataObj == IntPtr.Zero && TargetFileType == "*")
                {
                    throw new ArgumentException();
                }
                DataObject dataObject = new DataObject(Marshal.GetObjectForIUnknown(pDataObj));
                if (dataObject.ContainsFileDropList())
                {
                    StringCollection files = dataObject.GetFileDropList();
                    filelists = new string[files.Count];
                    files.CopyTo(filelists, 0);
                }
            }
            else if (TargetFileType == @"Directory\Background")
            {
                //余白をクリックされたフォルダパスを取得
                if (pidlFolder == IntPtr.Zero)
                {
                    throw new ArgumentException();
                }
                else
                {
                    var buff = new StringBuilder(260);
                    NativeMethods.SHGetPathFromIDList(pidlFolder, buff);
                    folderpath = buff.ToString();
                }
            }
            else
            {
                throw new ArgumentException();
            }
        }

        private uint IDM_SHOW_FILENAME = 0;

        public int QueryContextMenu(IntPtr hMenu, uint iMenu, uint idCmdFirst, uint idCmdLast, uint uFlags)
        {
            // メニュー項目を追加
            if (((uint)CMF.CMF_DEFAULTONLY & uFlags) != 0)
            {
                return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0);
            }

            //メニュー表示対象の拡張子でなければ追加しない
            if (!IsShowMenu())
            {
                return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0);
            }

            MENUITEMINFO mii = new MENUITEMINFO();
            mii.cbSize = (uint)Marshal.SizeOf(mii);
            mii.fMask = MIIM.MIIM_ID | MIIM.MIIM_TYPE | MIIM.MIIM_STATE;
            mii.wID = idCmdFirst + IDM_SHOW_FILENAME;
            mii.fType = MFT.MFT_STRING;
            mii.dwTypeData = MENU_NAME;
            mii.fState = MFS.MFS_ENABLED;
            if (!NativeMethods.InsertMenuItem(hMenu, iMenu, true, ref mii))
            {
                return Marshal.GetHRForLastWin32Error();
            }

            // 追加したメニュー項目の数を返す
            return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 1);
        }

        public void InvokeCommand(IntPtr pici)
        {
            CMINVOKECOMMANDINFO ici = (CMINVOKECOMMANDINFO)Marshal.PtrToStructure(pici, typeof(CMINVOKECOMMANDINFO));

            if (NativeMethods.HighWord(ici.lpVerb.ToInt32()) != 0)
            {
                return;
            }

            if (NativeMethods.LowWord(ici.lpVerb.ToInt32()) == IDM_SHOW_FILENAME)
            {
                if (TargetFileType == "*")
                {
                    if (filelists != null && filelists.Length > 0)
                    {
                        ActionMethod();
                    }
                }
                else if (TargetFileType == @"Directory\Background")
                {
                    if (folderpath != null && System.IO.Directory.Exists(folderpath))
                    {
                        ActionMethod();
                    }
                }
            }
            else
            {
                Marshal.ThrowExceptionForHR(WinError.E_FAIL);
            }
        }

        private string MSG_SHOW_FILENAME = "";

        public void GetCommandString(UIntPtr idCmd, uint uFlags, IntPtr pReserved, StringBuilder pszName, uint cchMax)
        {
            if (idCmd.ToUInt32() != IDM_SHOW_FILENAME)
            {
                return;
            }
            if ((GCS)uFlags == GCS.GCS_HELPTEXTW)
            {
                if (MSG_SHOW_FILENAME.Length > cchMax - 1)
                {
                    Marshal.ThrowExceptionForHR(WinError.STRSAFE_E_INSUFFICIENT_BUFFER);
                }
                else
                {
                    pszName.Clear();
                    pszName.Append(MSG_SHOW_FILENAME);
                }
            }
        }

        private const string Description = "ContextMenuExtention Class";

        [ComRegisterFunction()]
        public static void Register(Type t)
        {
            try
            {
                Guid clsid = t.GUID;
                if (clsid == Guid.Empty)
                {
                    throw new ArgumentException("clsid must not be empty");
                }
                string keyName = string.Format("{0}\\shellex\\ContextMenuHandlers\\{1}", TargetFileType, clsid.ToString("B"));
                using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(keyName))
                {
                    if (key != null && !string.IsNullOrEmpty(Description))
                    {
                        key.SetValue(null, Description);
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                throw;
            }
        }

        [ComUnregisterFunction()]
        public static void Unregister(Type t)
        {
            try
            {
                Guid clsid = t.GUID;
                if (clsid == Guid.Empty)
                {
                    throw new ArgumentException("clsid must not be empty");
                }
                string keyName = string.Format("{0}\\shellex\\ContextMenuHandlers\\{1}", TargetFileType, clsid.ToString("B"));
                Registry.ClassesRoot.DeleteSubKeyTree(keyName, false);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                throw;
            }
        }
    }

    [ComImport(),
    InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
    Guid("000214e8-0000-0000-c000-000000000046")]
    internal interface IShellExtInit
    {
        void Initialize(IntPtr /*LPCITEMIDLIST*/ pidlFolder, IntPtr /*LPDATAOBJECT*/ pDataObj, IntPtr /*HKEY*/ hKeyProgID);
    }

    [ComImport(),
    InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
    Guid("000214e4-0000-0000-c000-000000000046")]
    internal interface IContextMenu
    {
        [PreserveSig]
        int QueryContextMenu(IntPtr /*HMENU*/ hMenu, uint iMenu, uint idCmdFirst, uint idCmdLast, uint uFlags);
        void InvokeCommand(IntPtr pici);
        void GetCommandString(UIntPtr idCmd, uint uFlags, IntPtr pReserved, StringBuilder pszName, uint cchMax);
    }

    internal static class WinError
    {
        public const int S_OK = 0x0000;
        public const int S_FALSE = 0x0001;
        public const int E_FAIL = -2147467259;
        public const int E_INVALIDARG = -2147024809;
        public const int E_OUTOFMEMORY = -2147024882;
        public const int STRSAFE_E_INSUFFICIENT_BUFFER = -2147024774;

        public const uint SEVERITY_SUCCESS = 0;
        public const uint SEVERITY_ERROR = 1;

        public static int MAKE_HRESULT(uint sev, uint fac, uint code)
        {
            return (int)((sev << 31) | (fac << 16) | code);
        }
    }

    [Flags]
    internal enum MIIM : uint
    {
        MIIM_STATE = 0x00000001,
        MIIM_ID = 0x00000002,
        MIIM_SUBMENU = 0x00000004,
        MIIM_CHECKMARKS = 0x00000008,
        MIIM_TYPE = 0x00000010,
        MIIM_DATA = 0x00000020,
        MIIM_STRING = 0x00000040,
        MIIM_BITMAP = 0x00000080,
        MIIM_FTYPE = 0x00000100
    }

    internal enum MFT : uint
    {
        MFT_STRING = 0x00000000,
        MFT_BITMAP = 0x00000004,
        MFT_MENUBARBREAK = 0x00000020,
        MFT_MENUBREAK = 0x00000040,
        MFT_OWNERDRAW = 0x00000100,
        MFT_RADIOCHECK = 0x00000200,
        MFT_SEPARATOR = 0x00000800,
        MFT_RIGHTORDER = 0x00002000,
        MFT_RIGHTJUSTIFY = 0x00004000
    }

    internal enum MFS : uint
    {
        MFS_ENABLED = 0x00000000,
        MFS_UNCHECKED = 0x00000000,
        MFS_UNHILITE = 0x00000000,
        MFS_GRAYED = 0x00000003,
        MFS_DISABLED = 0x00000003,
        MFS_CHECKED = 0x00000008,
        MFS_HILITE = 0x00000080,
        MFS_DEFAULT = 0x00001000
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    internal struct MENUITEMINFO
    {
        public uint cbSize;
        public MIIM fMask;
        public MFT fType;
        public MFS fState;
        public uint wID;
        public IntPtr hSubMenu;
        public IntPtr hbmpChecked;
        public IntPtr hbmpUnchecked;
        public UIntPtr dwItemData;
        public string dwTypeData;
        public uint cch;
        public IntPtr hbmpItem;
    }

    [Flags]
    internal enum CMF : uint
    {
        CMF_NORMAL = 0x00000000,
        CMF_DEFAULTONLY = 0x00000001,
        CMF_VERBSONLY = 0x00000002,
        CMF_EXPLORE = 0x00000004,
        CMF_NOVERBS = 0x00000008,
        CMF_CANRENAME = 0x00000010,
        CMF_NODEFAULT = 0x00000020,
        CMF_INCLUDESTATIC = 0x00000040,
        CMF_ITEMMENU = 0x00000080,
        CMF_EXTENDEDVERBS = 0x00000100,
        CMF_DISABLEDVERBS = 0x00000200,
        CMF_ASYNCVERBSTATE = 0x00000400,
        CMF_OPTIMIZEFORINVOKE = 0x00000800,
        CMF_SYNCCASCADEMENU = 0x00001000,
        CMF_DONOTPICKDEFAULT = 0x00002000,
        CMF_RESERVED = 0xFFFF0000
    }

    [Flags]
    internal enum CMIC : uint
    {
        CMIC_MASK_ICON = 0x00000010,
        CMIC_MASK_HOTKEY = 0x00000020,
        CMIC_MASK_NOASYNC = 0x00000100,
        CMIC_MASK_FLAG_NO_UI = 0x00000400,
        CMIC_MASK_UNICODE = 0x00004000,
        CMIC_MASK_NO_CONSOLE = 0x00008000,
        CMIC_MASK_ASYNCOK = 0x00100000,
        CMIC_MASK_NOZONECHECKS = 0x00800000,
        CMIC_MASK_FLAG_LOG_USAGE = 0x04000000,
        CMIC_MASK_SHIFT_DOWN = 0x10000000,
        CMIC_MASK_PTINVOKE = 0x20000000,
        CMIC_MASK_CONTROL_DOWN = 0x40000000
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public struct POINT
    {
        public int X;
        public int Y;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    internal struct CMINVOKECOMMANDINFO
    {
        public uint cbSize;
        public CMIC fMask;
        public IntPtr hwnd;
        public IntPtr lpVerb;
        [MarshalAs(UnmanagedType.LPStr)]
        public string parameters;
        [MarshalAs(UnmanagedType.LPStr)]
        public string directory;
        public int nShow;
        public uint dwHotKey;
        public IntPtr hIcon;
    }
    internal enum GCS : uint
    {
        GCS_VERBA = 0x00000000,
        GCS_HELPTEXTA = 0x00000001,
        GCS_VALIDATEA = 0x00000002,
        GCS_VERBW = 0x00000004,
        GCS_HELPTEXTW = 0x00000005,
        GCS_VALIDATEW = 0x00000006,
        GCS_VERBICONW = 0x00000014,
        GCS_UNICODE = 0x00000004
    }

    internal class NativeMethods
    {
        [DllImport("user32", CharSet = CharSet.Unicode, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool InsertMenuItem(IntPtr hMenu, uint uItem, [MarshalAs(UnmanagedType.Bool)] bool fByPosition, ref MENUITEMINFO mii);

        [DllImport("shell32", CharSet = CharSet.Unicode)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool SHGetPathFromIDList(IntPtr pidlFolder, [MarshalAs(UnmanagedType.LPWStr), Out] StringBuilder lpPathName);

        public static int HighWord(int number)
        {
            return ((number & 0x80000000) == 0x80000000) ? (number >> 16) : ((number >> 16) & 0xffff);
        }

        public static int LowWord(int number)
        {
            return number & 0xffff;
        }
    }
}
```

# 参考URL

全般  
* [IDataObject ポインタから DragQueryFiles する代わりに DataObject.GetFileDropList を使う #C# - Qiita](https://qiita.com/tinymouse/items/cc865dafef73fcdf2c8c)  
* [IContextMenu インターフェイスを実装する方法 - Win32 apps | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/win32/shell/how-to-implement-the-icontextmenu-interface)  
* [StringCollection.CopyTo(String[], Int32) メソッド (System.Collections.Specialized) | Microsoft Learn](https://learn.microsoft.com/ja-jp/dotnet/api/system.collections.specialized.stringcollection.copyto?view=net-7.0)  
* [Windowsの右クリックメニューから複数ファイルをまとめて開く - Turgenev's Wiki](https://turgenev.cloudfree.jp/wiki/Windows%E3%81%AE%E5%8F%B3%E3%82%AF%E3%83%AA%E3%83%83%E3%82%AF%E3%83%A1%E3%83%8B%E3%83%A5%E3%83%BC%E3%81%8B%E3%82%89%E8%A4%87%E6%95%B0%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E3%81%BE%E3%81%A8%E3%82%81%E3%81%A6%E9%96%8B%E3%81%8F)  

フォルダ  
* [右クリックによるドラッグ&ドロップ時のドロップ先のパスを取得する - Cube Lilac](https://clown.cube-soft.jp/entry/20111005/drop_path)  
* [c++ - How can I obtain the folder path when right-clicking the background of a folder and invoking a context menu using a shell extension? - Stack Overflow](https://stackoverflow.com/questions/76384401/how-can-i-obtain-the-folder-path-when-right-clicking-the-background-of-a-folder)  
* [SHGetPathFromIDListW 関数 (shlobj_core.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/win32/api/shlobj_core/nf-shlobj_core-shgetpathfromidlistw)  
* [パスの最大長の制限 - Win32 apps | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/win32/fileio/maximum-file-path-limitation?tabs=registry)  

その他  
* [進め！中級プログラマー 第２号](https://www.asahi-net.or.jp/~gv4j-sgur/narik/stp002b.htm)  
* [Is it possible to set the position of the menu item in the context menu? · Issue #313 · dwmkerr/sharpshell · GitHub](https://github.com/dwmkerr/sharpshell/issues/313)  
* [コンテキストメニューCOMオブジェクトの実装のコード例 - Win32 apps | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/win32/ad/example-code-for-implementation-of-the-context-menu-com-object)