
Shell Extensions for Windows Explorer
To improve the convenience of the developed products, we try to provide the maximum level of integration of functionality into the operating system so that it is convenient for the user to use the full potential of the application. This article will discuss the theoretical and practical aspects of developing Shell Extensions, components that can be integrated into the shell of the Windows operating system. As an example, consider the extension of the context menu list for files, as well as an overview of existing solutions in this area.
Generally speaking, there are a lot of options for integration components of the Windows operating system shell, for example: control panel applets, screen savers, and more, but in this article I would like to dwell on the expansion options of Windows Explorer, an OS component that I had to expand more than others, due to its functional load.
Windows Explorer allows you to expand yourself through the use of specialized COM objects. The Windows API contains interfaces and structures describing how such COM objects should work, what methods should export. After the COM object that implements the desired functionality is developed, it is registered at a specific path in the Windows registry, so that Windows Explorer, when executing the functionality described during registration, accesses the corresponding COM object.
So, we will start developing a COM object that allows you to expand the list of context menus for files.
We will develop using the .net framework.
We add the following directives to Assembly.cs that allow us to use our assembly as a COM object:
We will need to import some features of the Windows API.
The DragQueryFile function will allow us to get a list of files selected in the directory, SHChangeNotify to notify the operating system that the shell has been changed.
Since we are developing a COM object that extends the context menu, we must implement the IShellExtInit interface. In the Initialize method, we get basic information about the directory in which we are executing.
It is also necessary to describe and implement the IContextMenu COM interface. A value of PreserveSig equal to true initiates the direct conversion of unmanaged signatures with HRESULT or retval values, and false causes automatic conversion of HRESULT or retval values to exceptions. The default value for the PreserveSig field is true.
The QueryContextMenu method will be called when the context menu is called, in it we will need to implement the functionality for adding a menu item, GetCommandString will return some details for this command, its description and so on. InvokeCommand will be invoked when you select the menu item that we will add.
For a COM object, it is also necessary to implement the install and uninstall functions.
In this function, we register our component in the registry, since we have a component that extends the functionality of the context menu, we register in the ContextMenuHandlers section (* \\ shellex \\ ContextMenuHandlers \\ ExShell). After registration, we restart the explorer.exe process so that our changes take effect immediately.
The function of removing the component, we clear the registry from previously created keys.
Next, we move on to the process of implementing interface functions. We implement the interfaces IShellExtInit, IContextMenu. I will not describe in detail the entire code of this class, I will focus on the implementation of the functions of these interfaces.
The component initialization function is launched when a directory or any other object is opened, in the context of which there may be a context menu. We use the IDataObject interface to obtain data about the current object, in particular, we are interested in hGlobal. This Handle identifies the current object within which our execution takes place.
Next, consider the function that is called when the context menu pops up.
In this section of the code, we verify that we are executing in the right context and try to request the number of files selected in the directory, as well as save a list of these files. I also note that when passing iFile = 0xffffffff to the DragQueryFile function, it will return the number of files in the directory.
Here we add a new menu item by calling the InsertMenuItem function, then we prepare and add an icon to this menu item, as well as a dividing line for aesthetic beauty. The structure of MENUITEMINFO describes our menu item, namely its type (ftype), data contained (dwTypeData), status (fState), menu item identifier (wID). The hMenu variable identifies the current drop-down menu, iMenu the position at which we are added. In order to get more complete information, you can contact MSDN.
Next, consider the GetCommandString function.
This function returns a language-independent description of the command, as well as a brief hint in the form of helptext, respectively.
Well, the last function that will be called when choosing our menu item:
Everything is quite transparent here.
Since our COM object is executed in the context of Windows Explorer, in order to debug it, we need to connect to the explorer.exe process. To register and remove the component, you can use the bat files supplied with the source code for this article. For registration, we use the RegAsm.exe utility that allows COM clients to use the .net class as if it is COM, as well as GacUtil in order to put the assembly in the GAC. After registration, the process explorer.exe will be restarted.
I will also draw your attention to a utility that allows you to view, and if necessary, edit all the Windows Explorer extensions installed in the system. The utility is called ShellExView, you can download it on the manufacturer's website Nirsoft or in the appendix to the article.
This is what our component looks like in ShellExView:

It looks like this when the context menu is open:

So, we have examined an example of developing a component for extending Windows Explorer, but this type of extension is far from the only thing that we can change and what we can influence.
With an understanding of how these kinds of components function, you can look at what has already been developed by the community and can be used to facilitate the implementation of such operations.
For example, the SharpShell library allows you to perform a fairly large amount of modifications, as well as well described in the .NET Shell Extensions article series on CodeProject. You can also use the Windows Shell Framework library as an analogor a library for ATL + C ++ bundles Mini Shell Extension Framework .
I will also draw your attention to a warning from Microsoft regarding the development of such extensions: “It is not recommended to write shell extensions in .NET languages, since only one CLR runtime is used for one process, therefore, a conflict may arise between two shell extensions using different versions of the CLR. However .Net Framework 4 supports side-by-side technology for versions of .Net Framework 2.0, 3.0, 3.5 and allows you to use both the old CLR 2 and the new CLR 4 in the same process. ”
Read more about the limitations of using .net when developing Shell Extensions here: Guidance for Implementing In-Process Extensions
See also:
Explorer column handler shell extension in C #
Creating Shell Extension Handlers
Sources and utilities: ExampleShell.rar
PS The extension was not tested on Windows 8, judging by the reviews, for the registry to work correctly, you need to set the following in the HKEY_CLASSES_ROOT \ CLSID \ {guid component} \ InprocServer32 section ThreadingModel = Apartment.
Thanks for attention!
Generally speaking, there are a lot of options for integration components of the Windows operating system shell, for example: control panel applets, screen savers, and more, but in this article I would like to dwell on the expansion options of Windows Explorer, an OS component that I had to expand more than others, due to its functional load.
Windows Explorer allows you to expand yourself through the use of specialized COM objects. The Windows API contains interfaces and structures describing how such COM objects should work, what methods should export. After the COM object that implements the desired functionality is developed, it is registered at a specific path in the Windows registry, so that Windows Explorer, when executing the functionality described during registration, accesses the corresponding COM object.
So, we will start developing a COM object that allows you to expand the list of context menus for files.
We will develop using the .net framework.
We add the following directives to Assembly.cs that allow us to use our assembly as a COM object:
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(true)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("345F4DC2-A9BF-11E2-AA47-CC986188709B")]
We will need to import some features of the Windows API.
[DllImport("shell32")]
internal static extern uint DragQueryFile(uint hDrop,uint iFile, StringBuilder buffer, int cch);
[DllImport("user32")]
internal static extern uint CreatePopupMenu();
[DllImport("user32")]
internal static extern int InsertMenuItem(uint hmenu, uint uposition, uint uflags, ref MENUITEMINFO mii);
[DllImport("user32.dll")]
internal static extern bool SetMenuItemBitmaps(IntPtr hMenu, uint uPosition,
uint uFlags, IntPtr hBitmapUnchecked, IntPtr hBitmapChecked);
[DllImport("Shell32.dll")]
internal static extern void SHChangeNotify(int wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
const int SHCNE_ASSOCCHANGED = 0x08000000;
[DllImport("user32.dll", SetLastError = true)]
internal static extern bool PostMessage(IntPtr hWnd, [MarshalAs(UnmanagedType.U4)] uint Msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
internal static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
The DragQueryFile function will allow us to get a list of files selected in the directory, SHChangeNotify to notify the operating system that the shell has been changed.
Since we are developing a COM object that extends the context menu, we must implement the IShellExtInit interface. In the Initialize method, we get basic information about the directory in which we are executing.
[ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), GuidAttribute("000214e8-0000-0000-c000-000000000046")]
public interface IShellExtInit
{
[PreserveSig()]
int Initialize (IntPtr pidlFolder, IntPtr lpdobj, uint /*HKEY*/ hKeyProgID);
}
It is also necessary to describe and implement the IContextMenu COM interface. A value of PreserveSig equal to true initiates the direct conversion of unmanaged signatures with HRESULT or retval values, and false causes automatic conversion of HRESULT or retval values to exceptions. The default value for the PreserveSig field is true.
[ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), GuidAttribute("000214e4-0000-0000-c000-000000000046")]
public interface IContextMenu
{
// IContextMenu methods
[PreserveSig()]
int QueryContextMenu(uint hmenu, uint iMenu, int idCmdFirst, int idCmdLast, uint uFlags);
[PreserveSig()]
void InvokeCommand (IntPtr pici);
[PreserveSig()]
void GetCommandString(int idCmd, uint uFlags, int pwReserved, [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 4)] byte[] pszName, uint cchMax);
}
The QueryContextMenu method will be called when the context menu is called, in it we will need to implement the functionality for adding a menu item, GetCommandString will return some details for this command, its description and so on. InvokeCommand will be invoked when you select the menu item that we will add.
For a COM object, it is also necessary to implement the install and uninstall functions.
[System.Runtime.InteropServices.ComRegisterFunctionAttribute()]
static void RegisterServer(System.Type type)
{
try
{
string approved = string.Empty;
string contextMenu = string.Empty;
RegistryKey root;
RegistryKey rk;
root = Registry.LocalMachine;
rk = root.OpenSubKey(Resources.ApprovedReg, true);
rk.SetValue(type.GUID.ToString("B"), Resources.Extension);
approved = rk.ToString();
rk.Flush();
rk.Close();
root = Registry.ClassesRoot;
rk = root.CreateSubKey(Resources.ExShellReg);
rk.Flush();
rk.SetValue(null, type.GUID.ToString("B"));
contextMenu = rk.ToString();
rk.Flush();
rk.Close();
EventLog.WriteEntry("Application", "Example ShellExt Registration Complete.\r\n" + approved + "\r\n" + contextMenu, EventLogEntryType.Information);
RestartExplorer();
}
catch(Exception e)
{
EventLog.WriteEntry("Application", "Example ShellExt Registration error.\r\n" + e.ToString(), EventLogEntryType.Error);
}
}
In this function, we register our component in the registry, since we have a component that extends the functionality of the context menu, we register in the ContextMenuHandlers section (* \\ shellex \\ ContextMenuHandlers \\ ExShell). After registration, we restart the explorer.exe process so that our changes take effect immediately.
[System.Runtime.InteropServices.ComUnregisterFunctionAttribute()]
static void UnregisterServer(System.Type type)
{
try
{
string approved = string.Empty;
string contextMenu = string.Empty;
RegistryKey root;
RegistryKey rk;
// Remove ShellExtenstions registration
root = Registry.LocalMachine;
rk = root.OpenSubKey(Resources.ApprovedReg, true);
approved = rk.ToString();
rk.DeleteValue(type.GUID.ToString("B"));
rk.Close();
// Delete regkey
root = Registry.ClassesRoot;
contextMenu = Resources.ExShellReg;
root.DeleteSubKey(Resources.ExShellReg);
EventLog.WriteEntry("Application", "Example ShellExt Unregister Complete.\r\n" + approved + "\r\n" + contextMenu, EventLogEntryType.Information);
Helpers.SHChangeNotify(0x08000000, 0, IntPtr.Zero, IntPtr.Zero);
}
catch(Exception e)
{
EventLog.WriteEntry("Application", "Example ShellExt Unregister error.\r\n" + e.ToString(), EventLogEntryType.Error);
}
}
The function of removing the component, we clear the registry from previously created keys.
Next, we move on to the process of implementing interface functions. We implement the interfaces IShellExtInit, IContextMenu. I will not describe in detail the entire code of this class, I will focus on the implementation of the functions of these interfaces.
int IShellExtInit.Initialize (IntPtr pidlFolder, IntPtr lpdobj, uint hKeyProgID)
{
try
{
if (lpdobj != (IntPtr)0)
{
// Get info about the directory
IDataObject dataObject = (IDataObject)Marshal.GetObjectForIUnknown(lpdobj);
FORMATETC fmt = new FORMATETC();
fmt.cfFormat = CLIPFORMAT.CF_HDROP;
fmt.ptd = 0;
fmt.dwAspect = DVASPECT.DVASPECT_CONTENT;
fmt.lindex = -1;
fmt.tymed = TYMED.TYMED_HGLOBAL;
STGMEDIUM medium = new STGMEDIUM();
dataObject.GetData(ref fmt, ref medium);
m_hDrop = medium.hGlobal;
}
}
catch(Exception)
{
}
return 0;
}
The component initialization function is launched when a directory or any other object is opened, in the context of which there may be a context menu. We use the IDataObject interface to obtain data about the current object, in particular, we are interested in hGlobal. This Handle identifies the current object within which our execution takes place.
Next, consider the function that is called when the context menu pops up.
int IContextMenu.QueryContextMenu(uint hMenu, uint iMenu, int idCmdFirst, int idCmdLast, uint uFlags)
{
if ( (uFlags & 0xf) == 0 || (uFlags & (uint)CMF.CMF_EXPLORE) != 0)
{
uint nselected = Helpers.DragQueryFile(m_hDrop, 0xffffffff, null, 0);
if (nselected > 0)
{
for (uint i = 0; i < nselected; i++)
{
StringBuilder sb = new StringBuilder(1024);
Helpers.DragQueryFile(m_hDrop, i, sb, sb.Capacity + 1);
fileNames.Add(sb.ToString());
}
}
else
return 0;
In this section of the code, we verify that we are executing in the right context and try to request the number of files selected in the directory, as well as save a list of these files. I also note that when passing iFile = 0xffffffff to the DragQueryFile function, it will return the number of files in the directory.
// Add the popup to the context menu
MENUITEMINFO mii = new MENUITEMINFO();
mii.cbSize = 48;
mii.fMask = (uint) MIIM.ID | (uint)MIIM.TYPE | (uint) MIIM.STATE;
mii.wID = idCmdFirst;
mii.fType = (uint) MF.STRING;
mii.dwTypeData = Resources.MenuItem;
mii.fState = (uint) MF.ENABLED;
Helpers.InsertMenuItem(hMenu, (uint)iMenu, (uint)MF.BYPOSITION | (uint)MF.STRING, ref mii);
commands.Add(idCmdFirst);
System.Reflection.Assembly myAssembly = System.Reflection.Assembly.GetExecutingAssembly();
Stream myStream = myAssembly.GetManifestResourceStream(Resources.BitmapName);
Bitmap image = new Bitmap(myStream);
Color backColor = image.GetPixel(1, 1);
image.MakeTransparent(backColor);
Helpers.SetMenuItemBitmaps((IntPtr)hMenu, (uint)iMenu, (uint)MF.BYPOSITION, image.GetHbitmap(), image.GetHbitmap());
// Add a separator
MENUITEMINFO sep = new MENUITEMINFO();
sep.cbSize = 48;
sep.fMask = (uint )MIIM.TYPE;
sep.fType = (uint) MF.SEPARATOR;
Helpers.InsertMenuItem(hMenu, iMenu + 1, 1, ref sep);
}
return 1;
}
Here we add a new menu item by calling the InsertMenuItem function, then we prepare and add an icon to this menu item, as well as a dividing line for aesthetic beauty. The structure of MENUITEMINFO describes our menu item, namely its type (ftype), data contained (dwTypeData), status (fState), menu item identifier (wID). The hMenu variable identifies the current drop-down menu, iMenu the position at which we are added. In order to get more complete information, you can contact MSDN.
Next, consider the GetCommandString function.
void IContextMenu.GetCommandString(int idCmd, uint uFlags, int pwReserved, byte[] pszName, uint cchMax)
{
string commandString = String.Empty;
switch(uFlags)
{
case (uint)GCS.VERB:
commandString = "test";
break;
case (uint)GCS.HELPTEXTW:
commandString = "test";
break;
}
var buf = Encoding.Unicode.GetBytes(commandString);
int cch = Math.Min(buf.Length, pszName.Length - 1);
if (cch > 0)
{
Array.Copy(buf, 0, pszName, 0, cch);
}
else
{
// null terminate the buffer
pszName[0] = 0;
}
}
This function returns a language-independent description of the command, as well as a brief hint in the form of helptext, respectively.
Well, the last function that will be called when choosing our menu item:
void IContextMenu.InvokeCommand (IntPtr pici)
{
try
{
System.Windows.Forms.MessageBox.Show("Test code");
}
catch(Exception exe)
{
EventLog.WriteEntry("Application", exe.ToString());
}
}
Everything is quite transparent here.
Since our COM object is executed in the context of Windows Explorer, in order to debug it, we need to connect to the explorer.exe process. To register and remove the component, you can use the bat files supplied with the source code for this article. For registration, we use the RegAsm.exe utility that allows COM clients to use the .net class as if it is COM, as well as GacUtil in order to put the assembly in the GAC. After registration, the process explorer.exe will be restarted.
I will also draw your attention to a utility that allows you to view, and if necessary, edit all the Windows Explorer extensions installed in the system. The utility is called ShellExView, you can download it on the manufacturer's website Nirsoft or in the appendix to the article.
This is what our component looks like in ShellExView:

It looks like this when the context menu is open:

So, we have examined an example of developing a component for extending Windows Explorer, but this type of extension is far from the only thing that we can change and what we can influence.
With an understanding of how these kinds of components function, you can look at what has already been developed by the community and can be used to facilitate the implementation of such operations.
For example, the SharpShell library allows you to perform a fairly large amount of modifications, as well as well described in the .NET Shell Extensions article series on CodeProject. You can also use the Windows Shell Framework library as an analogor a library for ATL + C ++ bundles Mini Shell Extension Framework .
I will also draw your attention to a warning from Microsoft regarding the development of such extensions: “It is not recommended to write shell extensions in .NET languages, since only one CLR runtime is used for one process, therefore, a conflict may arise between two shell extensions using different versions of the CLR. However .Net Framework 4 supports side-by-side technology for versions of .Net Framework 2.0, 3.0, 3.5 and allows you to use both the old CLR 2 and the new CLR 4 in the same process. ”
Read more about the limitations of using .net when developing Shell Extensions here: Guidance for Implementing In-Process Extensions
See also:
Explorer column handler shell extension in C #
Creating Shell Extension Handlers
Sources and utilities: ExampleShell.rar
PS The extension was not tested on Windows 8, judging by the reviews, for the registry to work correctly, you need to set the following in the HKEY_CLASSES_ROOT \ CLSID \ {guid component} \ InprocServer32 section ThreadingModel = Apartment.
Thanks for attention!