📜 ⬆️ ⬇️

Shell Extensions Development for Windows Explorer

To enhance the convenience of the products being developed, we try to ensure the maximum level of integration of the functionality into the operating system, so that the user is comfortable using the full potential of the application. This article will discuss the theoretical and practical aspects of developing Shell Extensions, components that allow integration into the shell of the Windows operating system. As an example, consider the expansion of the context menu list for files, as well as review the existing solutions in this area.


Generally speaking, there are a huge variety of options for integrating the components of the Windows operating system shell, for example: control panel applets, screen savers, and so on, but in this article I would like to dwell in more detail on the possibilities of expanding Windows Explorer, an OS component that I had to expand more than others. due to its functional load.

Windows Explorer allows you to extend yourself by using specialized COM objects. The Windows API contains interfaces and structures describing how such COM objects should work, which methods should be exported. After the COM object that implements the necessary functionality is developed, it is registered along a specific path in the Windows registry, so that Windows Explorer, when performing the described functionality, accesses the corresponding COM object.
So, let's start the development of a COM object allowing us to expand the list of the context menu for files.
We will develop using the .net framework.
')
Add the following directives to Assembly.cs that allow using 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 need to import some of the Windows API functions.
  [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 notify the operating system that the shell has been changed.

Since we are developing a COM object that extends the context menu, we need to implement the IShellExtInit interface. In the Initialize method, we get the basic information about the directory in which we run.

  [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 the PreserveSig field, which is equal to true, triggers a direct conversion of unmanaged signatures with HRESULT or retval values, and a value of false causes the 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 of adding a menu item, GetCommandString will return some details on this command, its description and so on. InvokeCommand will be called when you select the menu item that we add.

For a COM object, you also need to implement the install and delete 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 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 deleting a component, clears the registry from the keys created earlier.
Next, go to the process of implementing interface functions. We implement the interfaces IShellExtInit, IContextMenu. I will not describe in detail all the code of this class, I will dwell 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 starts when a directory or any other object is opened, in the context of which a context menu may be located. We use the IDataObject interface to get 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, we consider the function that is called when the context menu is dropped.

 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 check that we are running in the right context and are trying to query the number of files selected in the directory, as well as save the list of these files. Also note that when iFile = 0xffffffff is passed 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 MENUITEMINFO structure describes our menu item, namely its type (ftype), the contained data (dwTypeData), state (fState), menu item identifier (wID). The variable hMenu identifies the current drop-down menu, the iMenu 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 you select our menu item:
  void IContextMenu.InvokeCommand (IntPtr pici) { try { System.Windows.Forms.MessageBox.Show("Test code"); } catch(Exception exe) { EventLog.WriteEntry("Application", exe.ToString()); } } 

It's all quite transparent.

Since our COM object runs in the context of Windows Explorer, in order to debug it, we need to connect to the explorer.exe process. To register and delete a component, you can use the bat files supplied with the sources for this article. For registration, we use the RegAsm.exe utility, which allows COM clients to use the .net class as if it were COM, and also GacUtil to place the assembly in the GAC. After registration, the explorer.exe process will be restarted.
I would also like to draw your attention to the 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 Nirsoft manufacturer’s website or in the annex to the article.

This is what our component looks like in ShellExView:


So it looks when the context menu is opened:


So, we looked at an example of developing a component of the expanding Windows Explorer, but this type of extension is far from the only thing that we can change and influence.
Having an understanding of how such components function, you can look at what has already been developed by the community and can be used to facilitate the execution 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 series of articles on CodeProject. As an analogue, you can also use the Windows Shell Framework library or the library for the ATL + C ++ Mini Shell Extension Framework .

I would also like to draw your attention to the 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, so there may be a conflict between two shell extensions using different CLR versions. However, .Net Framework 4 supports side-by-side technology for .Net Framework 2.0, 3.0, 3.5 versions and allows using the old CLR 2 and the new CLR 4 in the same process. ”
You can read more about limitations of using .net when developing Shell Extensions here: Guidance for Implementing In-Process Extensions

Another look:
Explorer column handler shell extension in C #
Creating Shell Extension Handlers


Sources and utilities: ExampleShell.rar

PS The extension did not test on Windows 8, judging by the reviews, to work correctly in the registry, in the HKEY_CLASSES_ROOT \ CLSID \ {component guid} \ InprocServer32 section, set the following value to ThreadingModel = Apartment.

Thanks for attention!

Source: https://habr.com/ru/post/177469/


All Articles