.NET で Windows エクスプローラのコンテキストメニューを拡張

# はじめに

僕は以前 Delphi で Windows のシェル拡張（Shell Extension）を使ってエクスプローラに機能を追加するアプリを作りました。
[簡単エクスプローラ拡張 EzExpEx]
(http://hp.vector.co.jp/authors/VA029585/EzExpEx/)
これは残念なことに 64 ビット Windows のエクスプローラで動かないんですよね。僕が持っている Delphi では 64 ビット Windows アプリを作れないんです。
これを VB.NET で作り直したいとずっと思っていました。

そのために調べていると次の記事がありました。
[How to Write Windows Shell Extension with .NET Languages - CodeProject]
(http://www.codeproject.com/Articles/174369/How-to-Write-Windows-Shell-Extension-with-NET-Lang)
これを参考にしてプログラムを書いてみました。

# Windows シェル拡張でコンテキストメニュー

エクスプローラで右クリックするとコンテキストメニューが表示されます。このメニューに独自の項目を追加します。

## プロジェクトを作る

まず、「クラスライブラリ」のプロジェクトを新規作成します。

ここではプロジェクト名を「ContextMenuExtension」にします。

次に、プロジェクトの「参照」設定に以下のアセンブリを追加します。

* System.Windows.Forms
* System.Drawing

IDE が作成したソースファイル「Class1.cs」または「Class1.vb」を「ContextMenuExtension.cs」または「ContextMenuExtension.vb」にリネームします。* 好きなファイル名で構いません。

## IShellExtInit と IContextMenu を用意する

```csharp:ContextMenuExtension.cs
using System.Runtime.InteropServices;
using System.Text;

namespace ContextMenuExtension
{
    [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);
    }
```

```vbnet:ContextMenuExtension.vb
Imports System.Runtime.InteropServices
Imports System.Text

<ComImport(),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("000214e8-0000-0000-c000-000000000046")>
Friend Interface IShellExtInit
    Sub Initialize(ByVal pidlFolder As IntPtr, ByVal pDataObj As IntPtr, ByVal hKeyProgID As IntPtr)
End Interface

<ComImport(),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("000214e4-0000-0000-c000-000000000046")>
Friend Interface IContextMenu
    <PreserveSig()>
    Function QueryContextMenu(ByVal hMenu As IntPtr, ByVal iMenu As UInt32, ByVal idCmdFirst As UInt32, ByVal idCmdLast As UInt32, ByVal uFlags As UInt32) As Integer
    Sub InvokeCommand(ByVal pici As IntPtr)
    Sub GetCommandString(ByVal idCmd As UIntPtr, ByVal uFlags As UInt32, ByVal pReserved As IntPtr, ByVal pszName As StringBuilder, ByVal cchMax As UInt32)
End Interface
```

## IShellExtInit と IContextMenu から派生する

「ContextMenuExtension」クラスを「IShellExtInit」「IContextMenu」から派生するよう書換します。

```csharp:ContextMenuExtension.cs
    public class ContextMenuExtension : IShellExtInit, IContextMenu
    {
```

```vbnet:ContextMenuExtension.vb
Public Class ContextMenuExtension
    Implements IShellExtInit, IContextMenu
```

IDE が「インターフェイスの実装」を助言してくるので従って以下のメソッドを実装します。

* Initialize
* QueryContextMenu
* GetComandString
* InvokeCommand

## 必要な定数やメソッドを用意する

必要な定数やメソッドを用意します。

```csharp:ContextMenuExtension.cs
using System.Runtime.InteropServices.ComTypes;

namespace CsContextMenuExtension
{
    internal enum CLIPFORMAT : uint
    {
        CF_TEXT = 1,
        CF_BITMAP = 2,
        CF_METAFILEPICT = 3,
        CF_SYLK = 4,
        CF_DIF = 5,
        CF_TIFF = 6,
        CF_OEMTEXT = 7,
        CF_DIB = 8,
        CF_PALETTE = 9,
        CF_PENDATA = 10,
        CF_RIFF = 11,
        CF_WAVE = 12,
        CF_UNICODETEXT = 13,
        CF_ENHMETAFILE = 14,
        CF_HDROP = 15,
        CF_LOCALE = 16,
        CF_MAX = 17,

        CF_OWNERDISPLAY = 0x0080,
        CF_DSPTEXT = 0x0081,
        CF_DSPBITMAP = 0x0082,
        CF_DSPMETAFILEPICT = 0x0083,
        CF_DSPENHMETAFILE = 0x008E,

        CF_PRIVATEFIRST = 0x0200,
        CF_PRIVATELAST = 0x02FF,

        CF_GDIOBJFIRST = 0x0300,
        CF_GDIOBJLAST = 0x03FF
    }

    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 WinApi
    {
        public const int MAX_PATH = 260;

        [DllImport("shell32", CharSet = CharSet.Unicode)]
        public static extern uint DragQueryFile(IntPtr hDrop, uint iFile, StringBuilder pszFile, int cch);

        [DllImport("ole32.dll", CharSet = CharSet.Unicode)]
        public static extern void ReleaseStgMedium(ref STGMEDIUM pmedium);

        [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);

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

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

```vbnet:ContextMenuExtension.vb
Imports System.Runtime.InteropServices.ComTypes

Friend Enum CLIPFORMAT As UInt32
    CF_TEXT = 1
    CF_BITMAP = 2
    CF_METAFILEPICT = 3
    CF_SYLK = 4
    CF_DIF = 5
    CF_TIFF = 6
    CF_OEMTEXT = 7
    CF_DIB = 8
    CF_PALETTE = 9
    CF_PENDATA = 10
    CF_RIFF = 11
    CF_WAVE = 12
    CF_UNICODETEXT = 13
    CF_ENHMETAFILE = 14
    CF_HDROP = 15
    CF_LOCALE = &H10
    CF_MAX = &H11

    CF_OWNERDISPLAY = &H80
    CF_DSPTEXT = &H81
    CF_DSPBITMAP = &H82
    CF_DSPMETAFILEPICT = &H83
    CF_DSPENHMETAFILE = &H8E

    CF_PRIVATEFIRST = &H200
    CF_PRIVATELAST = &H2FF

    CF_GDIOBJFIRST = &H300
    CF_GDIOBJLAST = &H3FF
End Enum

Friend Class WinError
    Public Const S_OK As Integer = 0
    Public Const S_FALSE As Integer = 1
    Public Const E_FAIL As Integer = -2147467259
    Public Const E_INVALIDARG As Integer = -2147024809
    Public Const E_OUTOFMEMORY As Integer = -2147024882
    Public Const STRSAFE_E_INSUFFICIENT_BUFFER As Integer = -2147024774

    Public Const SEVERITY_ERROR As UInt32 = 1
    Public Const SEVERITY_SUCCESS As UInt32 = 0

    Public Shared Function MAKE_HRESULT(ByVal sev As UInt32, ByVal fac As UInt32, ByVal code As UInt32) As Integer
        Return CInt((((sev << &H1F) Or (fac << &H10)) Or code))
    End Function
End Class

<Flags()>
Friend Enum MIIM As UInt32
    MIIM_STATE = 1
    MIIM_ID = 2
    MIIM_SUBMENU = 4
    MIIM_CHECKMARKS = 8
    MIIM_TYPE = &H10
    MIIM_DATA = &H20
    MIIM_STRING = &H40
    MIIM_BITMAP = &H80
    MIIM_FTYPE = &H100
End Enum

Friend Enum MFT As UInt32
    MFT_STRING = 0
    MFT_BITMAP = 4
    MFT_MENUBARBREAK = &H20
    MFT_MENUBREAK = &H40
    MFT_OWNERDRAW = &H100
    MFT_RADIOCHECK = &H200
    MFT_SEPARATOR = &H800
    MFT_RIGHTORDER = &H2000
    MFT_RIGHTJUSTIFY = &H4000
End Enum

Friend Enum MFS As UInt32
    MFS_ENABLED = 0
    MFS_UNCHECKED = 0
    MFS_UNHILITE = 0
    MFS_DISABLED = 3
    MFS_GRAYED = 3
    MFS_CHECKED = 8
    MFS_HILITE = &H80
    MFS_DEFAULT = &H1000
End Enum

<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)>
Friend Structure MENUITEMINFO
    Public cbSize As UInt32
    Public fMask As MIIM
    Public fType As MFT
    Public fState As MFS
    Public wID As UInt32
    Public hSubMenu As IntPtr
    Public hbmpChecked As IntPtr
    Public hbmpUnchecked As IntPtr
    Public dwItemData As UIntPtr
    Public dwTypeData As String
    Public cch As UInt32
    Public hbmpItem As IntPtr
End Structure

<Flags()>
Friend Enum CMF
    CMF_NORMAL = 0
    CMF_DEFAULTONLY = 1
    CMF_VERBSONLY = 2
    CMF_EXPLORE = 4
    CMF_NOVERBS = 8
    CMF_CANRENAME = &H10
    CMF_NODEFAULT = &H20
    CMF_INCLUDESTATIC = &H40
    CMF_ITEMMENU = &H80
    CMF_EXTENDEDVERBS = &H100
    CMF_DISABLEDVERBS = &H200
    CMF_ASYNCVERBSTATE = &H400
    CMF_OPTIMIZEFORINVOKE = &H800
    CMF_SYNCCASCADEMENU = &H1000
    CMF_DONOTPICKDEFAULT = &H2000
    CMF_RESERVED = &HFFFF0000
End Enum

<Flags()>
Friend Enum CMIC As UInt32
    CMIC_MASK_ICON = &H10
    CMIC_MASK_HOTKEY = &H20
    CMIC_MASK_NOASYNC = &H100
    CMIC_MASK_FLAG_NO_UI = &H400
    CMIC_MASK_UNICODE = &H4000
    CMIC_MASK_NO_CONSOLE = &H8000
    CMIC_MASK_ASYNCOK = &H100000
    CMIC_MASK_NOZONECHECKS = &H800000
    CMIC_MASK_FLAG_LOG_USAGE = &H4000000
    CMIC_MASK_SHIFT_DOWN = &H10000000
    CMIC_MASK_PTINVOKE = &H20000000
    CMIC_MASK_CONTROL_DOWN = &H40000000
End Enum

<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)>
Public Structure POINT
    Public X As Integer
    Public Y As Integer
End Structure

<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)>
Friend Structure CMINVOKECOMMANDINFO
    Public cbSize As UInt32
    Public fMask As CMIC
    Public hwnd As IntPtr
    Public lpVerb As IntPtr
    <MarshalAs(UnmanagedType.LPStr)>
    Public parameters As String
    <MarshalAs(UnmanagedType.LPStr)>
    Public directory As String
    Public nShow As Integer
    Public dwHotKey As UInt32
    Public hIcon As IntPtr
End Structure

Friend Enum GCS As UInt32
    GCS_VERBA = 0
    GCS_HELPTEXTA = 1
    GCS_VALIDATEA = 2
    GCS_HELPTEXTW = 5
    GCS_UNICODE = 4
    GCS_VERBW = 4
    GCS_VALIDATEW = 6
    GCS_VERBICONW = 20
End Enum

Friend Class WinApi
    Public Const MAX_PATH As Integer = 260

    <DllImport("shell32", CharSet:=CharSet.Unicode)>
    Public Shared Function DragQueryFile(ByVal hDrop As IntPtr, ByVal iFile As UInt32, ByVal pszFile As StringBuilder, ByVal cch As Integer) As UInt32
    End Function

    <DllImport("ole32.dll", CharSet:=CharSet.Unicode)>
    Public Shared Sub ReleaseStgMedium(ByRef pmedium As STGMEDIUM)
    End Sub

    <DllImport("user32", CharSet:=CharSet.Unicode, SetLastError:=True)>
    Public Shared Function InsertMenuItem(ByVal hMenu As IntPtr, ByVal uItem As UInt32, <MarshalAs(UnmanagedType.Bool)> ByVal fByPosition As Boolean, ByRef mii As MENUITEMINFO) As <MarshalAs(UnmanagedType.Bool)> Boolean
    End Function

    Public Shared Function HighWord(ByVal number As Integer) As Integer
        Return If(((number And &H80000000) = &H80000000), (number >> &H10), ((number >> &H10) And &HFFFF))
    End Function

    Public Shared Function LowWord(ByVal number As Integer) As Integer
        Return (number And &HFFFF)
    End Function
End Class
```

## Initialize メソッドを実装する

選択されたファイル／フォルダを取得して変数にセットしておきます。

```csharp:ContextMenuExtension.cs
    public class ContextMenuExtension : IShellExtInit, IContextMenu
    {
        private string selectedFile;

        public void Initialize(IntPtr pidlFolder, IntPtr pDataObj, IntPtr hKeyProgID)
        {
            // 選択されているファイル/ディレクトリを取得
            if (pDataObj == IntPtr.Zero)
            {
                throw new ArgumentException();
            }
            FORMATETC fe = new FORMATETC();
            fe.cfFormat = (short)CLIPFORMAT.CF_HDROP;
            fe.ptd = IntPtr.Zero;
            fe.dwAspect = DVASPECT.DVASPECT_CONTENT;
            fe.lindex = -1;
            fe.tymed = TYMED.TYMED_HGLOBAL;
            STGMEDIUM stm = new STGMEDIUM();
            IDataObject dataObject = (IDataObject)Marshal.GetObjectForIUnknown(pDataObj);
            dataObject.GetData(ref fe, out stm);
            try
            {
                IntPtr hDrop = stm.unionmember;
                if (hDrop == IntPtr.Zero)
                {
                    throw new ArgumentException();
                }
                uint nFiles = WinApi.DragQueryFile(hDrop, UInt32.MaxValue, null, 0);
                if (nFiles == 1)
                {
                    StringBuilder fileName = new StringBuilder(WinApi.MAX_PATH);
                    if (0 == WinApi.DragQueryFile(hDrop, 0, fileName,
                        fileName.Capacity))
                    {
                        Marshal.ThrowExceptionForHR(WinError.E_FAIL);
                    }
                    this.selectedFile = fileName.ToString();
                }
                else
                {
                    Marshal.ThrowExceptionForHR(WinError.E_FAIL);
                }
            }
            finally
            {
                WinApi.ReleaseStgMedium(ref stm);
            }
        }
```

```vbnet:ContextMenuExtension.vb
Public Class ContextMenuExtension
    Implements IShellExtInit, IContextMenu

    Private SelectedFile As String

    Public Sub Initialize(ByVal pidlFolder As IntPtr, ByVal pDataObj As IntPtr, ByVal hKeyProgID As IntPtr) Implements IShellExtInit.Initialize

        '選択されているファイル/ディレクトリを取得
        If (pDataObj = IntPtr.Zero) Then
            Throw New ArgumentException
        End If
        Dim fe As New FORMATETC
        With fe
            .cfFormat = CLIPFORMAT.CF_HDROP
            .ptd = IntPtr.Zero
            .dwAspect = DVASPECT.DVASPECT_CONTENT
            .lindex = -1
            .tymed = TYMED.TYMED_HGLOBAL
        End With
        Dim stm As New STGMEDIUM
        Dim dataObject As IDataObject = Marshal.GetObjectForIUnknown(pDataObj)
        dataObject.GetData(fe, stm)
        Try
            Dim hDrop As IntPtr = stm.unionmember
            If (hDrop = IntPtr.Zero) Then
                Throw New ArgumentException
            End If
            Dim nFiles As UInteger = WinApi.DragQueryFile(hDrop, UInt32.MaxValue, Nothing, 0)
            If (nFiles = 1) Then
                Dim fileName As New StringBuilder(WinApi.MAX_PATH)
                If (0 = WinApi.DragQueryFile(hDrop, 0, fileName, fileName.Capacity)) Then
                    Marshal.ThrowExceptionForHR(WinError.E_FAIL)
                End If
                Me.SelectedFile = fileName.ToString
            Else
                Marshal.ThrowExceptionForHR(WinError.E_FAIL)
            End If
        Finally
            WinApi.ReleaseStgMedium((stm))
        End Try
    End Sub
```

## QueryContextMenu メソッドを実装する

コンテキストメニューに項目を追加します。

```csharp:ContextMenuExtension.cs
    public class ContextMenuExtension : IShellExtInit, IContextMenu
    {
        private uint IDM_SHOW_FILENAME = 0;
        private string TXT_SHOW_FILENAME = "ファイル名を表示(&D)";

        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);
            }
            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 = TXT_SHOW_FILENAME;
            mii.fState = MFS.MFS_ENABLED;
            if (!WinApi.InsertMenuItem(hMenu, iMenu, true, ref mii))
            {
                return Marshal.GetHRForLastWin32Error();
            }

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

```vbnet:ContextMenuExtension.vb
Public Class ContextMenuExtension
    Implements IShellExtInit, IContextMenu

    Private IDM_SHOW_FILENAME As UInteger = 0
    Private TXT_SHOW_FILENAME As String = "ファイル名を表示(&D)"

    Public Function QueryContextMenu(ByVal hMenu As IntPtr, ByVal iMenu As UInt32, ByVal idCmdFirst As UInt32, ByVal idCmdLast As UInt32, ByVal uFlags As UInt32) As Integer Implements IContextMenu.QueryContextMenu

        'メニュー項目を追加
        If ((CMF.CMF_DEFAULTONLY And uFlags) <> 0) Then
            Return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0)
        End If
        Dim mii As New MENUITEMINFO
        With mii
            .cbSize = Marshal.SizeOf(mii)
            .fMask = MIIM.MIIM_TYPE Or MIIM.MIIM_STATE Or MIIM.MIIM_ID
            .wID = idCmdFirst + Me.IDM_SHOW_FILENAME
            .fType = MFT.MFT_STRING
            .dwTypeData = Me.TXT_SHOW_FILENAME
            .fState = MFS.MFS_ENABLED
        End With
        If Not WinApi.InsertMenuItem(hMenu, iMenu, True, mii) Then
            Return Marshal.GetHRForLastWin32Error
        End If

        '追加したメニュー項目の数を返す
        Return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 1))
    End Function
```

## GetCommandString メソッドを実装する

エクスプローラのステータスバーに表示されるメッセージを設定します。

```csharp:ContextMenuExtension.cs
    public class ContextMenuExtension : IShellExtInit, IContextMenu
    {
        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);
                }
            }
        }
```

```vbnet:ContextMenuExtension.vb
Public Class ContextMenuExtension
    Implements IShellExtInit, IContextMenu

    Private MSG_SHOW_FILENAME As String = "ファイル名を表示します。"

    Public Sub GetCommandString(ByVal idCmd As UIntPtr, ByVal uFlags As UInt32, ByVal pReserved As IntPtr, ByVal pszName As StringBuilder, ByVal cchMax As UInt32) Implements IContextMenu.GetCommandString

        If Not (idCmd.ToUInt32 = IDM_SHOW_FILENAME) Then
            Exit Sub
        End If
        If DirectCast(uFlags, GCS) = GCS.GCS_HELPTEXTW Then
            If (MSG_SHOW_FILENAME.Length > (cchMax - 1)) Then
                Marshal.ThrowExceptionForHR(WinError.STRSAFE_E_INSUFFICIENT_BUFFER)
            Else
                pszName.Clear()
                pszName.Append(MSG_SHOW_FILENAME)
            End If
        End If
    End Sub
```

## InvokeCommand メソッドを実装する

メニュー項目が選択されると呼出されます。

```csharp:ContextMenuExtension.cs
    public class ContextMenuExtension : IShellExtInit, IContextMenu
    {
        public void InvokeCommand(IntPtr pici)
        {
            CMINVOKECOMMANDINFO ici = (CMINVOKECOMMANDINFO)Marshal.PtrToStructure(pici, typeof(CMINVOKECOMMANDINFO));
            // ici.lpVerb の上位ワードが NULL でなければ
            // このメソッドは別のアプリケーションによって呼出されたものなので
            // そのまま返す
            if (WinApi.HighWord(ici.lpVerb.ToInt32()) != 0)
            {
                return;
            }

            // そうでなければ
            // シェルが呼出したもので
            // ici.lpVerb の下位ワードが、ユーザが選択したメニュー項目
            if (WinApi.LowWord(ici.lpVerb.ToInt32()) == IDM_SHOW_FILENAME)
            {
                this.DoShowFileName();
            }
            else
            {
                Marshal.ThrowExceptionForHR(WinError.E_FAIL);
            }
        }
```

```vbnet:ContextMenuExtension.vb
Public Class ContextMenuExtension
    Implements IShellExtInit, IContextMenu

    Public Sub InvokeCommand(ByVal pici As IntPtr) Implements IContextMenu.InvokeCommand

        Dim ici As CMINVOKECOMMANDINFO = Marshal.PtrToStructure(pici, GetType(CMINVOKECOMMANDINFO))
        'ici.lpVerb の上位ワードが NULL でなければ
        'このメソッドは別のアプリケーションによって呼出されたものなので
        'そのまま返す
        If (WinApi.HighWord(ici.lpVerb.ToInt32) <> 0) Then
            Exit Sub
        End If

        'そうでなければ
        'シェルが呼出したもので
        'ici.lpVerb の下位ワードが、ユーザが選択したメニュー項目
        If (WinApi.LowWord(ici.lpVerb.ToInt32) = IDM_SHOW_FILENAME) Then
            Me.DoShowFileName()
        Else
            Marshal.ThrowExceptionForHR(WinError.E_FAIL)
        End If
    End Sub
```

主な機能は「DoShowFileName」。選択したファイルの名称を表示します。

```csharp:ContextMenuExtension.cs
    public class ContextMenuExtension : IShellExtInit, IContextMenu
    {
        private void DoShowFileName()
        {
            System.Windows.Forms.MessageBox.Show("選択されたファイル: " + Environment.NewLine + this.selectedFile);
        }
```

```vbnet:ContextMenuExtension.vb
Public Class ContextMenuExtension
    Implements IShellExtInit, IContextMenu

    Private Sub DoShowFileName(hwnd As IntPtr)
        System.Windows.Forms.MessageBox.Show("選択されたファイル: " & Environment.NewLine & Me.SelectedFile, My.Application.Info.Title)
    End Sub
```

## COM サーバ登録機能を実装する

作成しているクラスライブラリが COM サーバとして登録できるようにします。

まず、アセンブリを COM 参照可能にします。以下の手順で作業します。

> プロジェクトの「プロパティ」を開く。
> 「アプリケーション」タブを選択。
> 「アセンブリ情報」を押下。
> ダイアログボックスで「アセンブリを COM 参照可能にする」にチェック。

次に、アセンブリに署名します。以下の手順で作業します。

> プロジェクトの「プロパティ」を開く。
> 「署名」タブを選択。
> 「アセンブリに署名する」をチェック。
> 「厳密な名前のキーファイルを選択」で「新規作成」を選択。
> ダイアログボックスで「キーファイル名」に任意のファイル名を指定。
> 「キーファイルをパスワードで保護する」は必須ではない。

クラスに属性を設定します。

GUID はクラスごとに新規作成して下さい。

```csharp:ContextMenuExtension.cs
    [ClassInterface(ClassInterfaceType.None),
    Guid("99999999-9999-9999-9999-9999999999"), 
    ComVisible(true)]
    public class ContextMenuExtension : IShellExtInit, IContextMenu
    {
```

```vbnet:ContextMenuExtension.vb
<ClassInterface(ClassInterfaceType.None),
Guid("99999999-9999-9999-9999-9999999999"),
ComVisible(True)>
Public Class ContextMenuExtension
    Implements IShellExtInit, IContextMenu
```

登録および解除のためのメソッドを実装します。

ここで対象となるファイルの拡張子を指定します。

```csharp:ContextMenuExtension.cs
using Microsoft.Win32

    public class ContextMenuExtension : IShellExtInit, IContextMenu
    {
        private const string Description = "ContextMenuExtention Class";
        private const string TargetFileType = "*";

        [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;
            }
        }
```

```vbnet:ContextMenuExtension.vb
Imports Microsoft.Win32

Public Class ContextMenuExtension
    Implements IShellExtInit, IContextMenu

    Private Const Description As String = "ContextMenuExtention Class"
    Private Const TargetFileType As String = "*"

    <ComRegisterFunction()>
    Public Shared Sub Register(ByVal t As Type)
        Try
            Dim clsid = t.GUID
            If clsid = Guid.Empty Then
                Throw New ArgumentException("clsid must not be empty")
            End If
            Dim keyName As String = String.Format("{0}\shellex\ContextMenuHandlers\{1}", TargetFileType, clsid.ToString("B"))
            Using key As RegistryKey = Registry.ClassesRoot.CreateSubKey(keyName)
                If ((key IsNot Nothing) AndAlso (Not String.IsNullOrEmpty(Description))) Then
                    key.SetValue(Nothing, Description)
                End If
            End Using
        Catch ex As Exception
            Console.WriteLine(ex.Message)
            Throw
        End Try
    End Sub

    <ComUnregisterFunction()>
    Public Shared Sub Unregister(ByVal t As Type)
        Try
            Dim clsid = t.GUID
            If clsid = Guid.Empty Then
                Throw New ArgumentException("clsid must not be empty")
            End If
            Dim keyName As String = String.Format("{0}\shellex\ContextMenuHandlers\{1}", TargetFileType, clsid.ToString("B"))
            Registry.ClassesRoot.DeleteSubKeyTree(keyName, False)
        Catch ex As Exception
            Console.WriteLine(ex.Message)
            Throw
        End Try
    End Sub
```

## ビルドする

ビルドするとアセンブリファイルが作成されます。これを COM サーバとして Windows に登録します。

## COM サーバとして登録する

C# または VB.NET で作成したアセンブリファイルを COM サーバとして登録するには、.NET Framework に同梱されている「regasm」ツールを使います。

```
regasm ContextMenuExtension.dll /codebase
```

# おわりに

以下の記事も書いています。
[.NET で Windows エクスプローラのコンテキストメニューを拡張 - Qiita] (http://qiita.com/tinymouse/items/eb8aebb39ddd5c103347)
上記のコードの大半をライブラリにした SharpShell を使用しています。