Shell extensions are a powerful and flexible way to extend Windows Shell capabilities. However, when working with Shell extension handlers you can encounter hidden difficulties.
In this article, we describe the general approach to creating Windows Shell extensions based on the example of shortcut menu and icon overlay handlers. We also explain a number of pitfalls you may face when developing these types of extensions and offer several best practices to avoid them.
Contents:
- What are Shell extensions?
- Creating Shell extension handlers
- Registering Shell extensions
- Installing Shell extensions
- Removing Shell extensions
- Common problems and their solutions
- Conclusion
What are Shell extensions?
What is a Shell extension? To define Shell extensions, let’s look at the Windows Shell extension handler that allows you to extend the usual set of actions while working with Windows Explorer. Shell extensions can be represented as individual plug-ins to Windows Explorer. They can be used to add a new tab to the Properties window, change a file preview, and do other things.
Before taking any action, the Shell calls registered extension handlers to customize this action. A typical example of such adjustment is a Shell extension handler for the shortcut menu.
Depending on the file type, Shell extension handlers can be added either to all types of objects within Windows Explorer or only to certain types of objects.
Shell extension handlers used with specific file types:
Shell extension handlers that don’t depend on the file type:
However, no matter what file type you apply a handler to, using Shell extensions can slow down Windows Explorer. In addition to Windows Explorer, other programs including Dropbox, TortoiseSVN, WinRAR, and SkyDrive establish their own sets of Shell extensions.
Creating Shell extension handlers
In this section, we’ll discuss the process of creating Windows Shell extension handlers based on the example of overlay and context menu extensions. Other types of Shell extensions can be implemented in a similar way.
Each Shell extension handler is a Component Object Model (COM) object. Handlers must have their own globally unique identifier (GUID) and be registered before the Shell can use them. The registry path is determined by the type of extension handler. Now let’s go through all the stages of creating a Shell extension handler.
Implementing the required functions and interfaces
Since a Shell extension handler is a COM object, it’s implemented as a dynamic link library (DLL). At the same time, just as any COM object, a DLL must export the following standard functions:
- DllMain – Creates an entry point to a DLL
- DllGetClassObject – Gets an object using the Class factory
- DllCanUnloadNow – Calls a DLL before unloading to check whether it’s currently being used
- DllRegisterServer – Registers a COM object in the registry
- DllUnregisterServer – Removes the COM object entry from the registry
The ClassFactory is used to create the component object and must implement the IClassFactory interface. Here’s how the ClassFactory looks:
C#
class ClassFactory : public IClassFactory
{
public:
// IUnknown
IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv);
IFACEMETHODIMP_(ULONG) AddRef();
IFACEMETHODIMP_(ULONG) Release();
// IClassFactory
IFACEMETHODIMP CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv);
IFACEMETHODIMP LockServer(BOOL fLock);
explicit ClassFactory();
~ClassFactory();
private:
long m_cRef;
};
In addition, the class that encapsulates the logic of the extension handler must implement the IUnknown interface, like all COM objects. Interfaces that are specific to different types of Shell extensions inherit from this interface. The IUnknown interface looks as follows:
C#
class IUnknown
{
public:
virtual HRESULT QueryInterface(REFID riid, void** ppv)=0;
virtual ULONG AddRef () = 0;
virtual ULONG Release() = 0;
};
Note that several extension handlers can be implemented in a single DLL. At the same time, you should specify GUID, perform registering in the registry, and set up a ClassFactory for each handler (although you can use a common ClassFactory for several extension handlers).
Definition of interfaces specific to Shell extensions
In addition to the required IUnknown COM interface, extension classes must implement an interface that’s specific to Shell extensions. For different types of extensions, there are different sets of required interfaces.
Most extension handlers must also implement either an IPersistFile or IShellExtInit interface in Windows XP or earlier. These were replaced by IInitializeWithStream, IInitializeWithItem, and IInitializeWithFile in Windows Vista. The Shell uses these interfaces to initialize the handler.
For a shortcut menu handler, the interfaces used are IShellExtInit and IContextMenu, and for the overlay icon handler, it’s the IShellIconOverlayIdentifier interface. Let’s look closer at each of them.
IShellExtInit
The IShellExtInit interface provides only an extension handler initialization function:
C#
class IShellExtInit : public IUnknown
{
public:
virtual HRESULT Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hKeyPro-gID) = 0;
};
The Initialize method is called by Windows Explorer before you perform an action that this handler extends. For example, in the case of extending the context menu, this method will be called after clicking on selected files and before the context menu opens.
In the Initialize method, you can implement logic that filters extension work depending on the type of selected files or any other conditions. Furthermore, the list of objects, which the user chooses, can be obtained in the Initialize method.
Note that you can use the IShellExtInit interface only when you’re writing a handler based on the IContextMenu or IShellPropSheetExt interface, as other Shell extensions based on different interfaces use different initialization methods.
IContextMenu
The IContextMenu interface contains methods for working with the context menu:
C#
class IContextMenu : public IUnknown
{
public:
virtual HRESULT QueryContextMenu(HMENU hMenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags) = 0;
virtual HRESULT InvokeCommand(LPCMINVOKECOMMANDINFO pici) = 0;
virtual HRESULT GetCommandString(UINT_PTR idCommand, UINT uFlags, UINT *pwReserved, LPSTR pszName, UINT cchMax) = 0;
};
The QueryContextMenu method is responsible for adding new menu items to the context menu. This is where you can add logic for creating new menu items. A menu item can be created by the WinApi InsertMenuItem function.
Each menu item is linked to its ID. The idCmdFirst parameter is the minimum ID value that can be assigned to a menu item. Accordingly, idCmdLast is the maximum ID value. It’s important that if a menu item contains a submenu, the submenu items are assigned to IDs created after the ID of the menu item.
The QueryContextMenu method returns the difference between the first and last assigned ID plus 1.
QueryContextMenu is called by Windows Explorer only if the extension is registered for separate file types.
The InvokeCommand method processes clicks on added menu items. Each menu button is identified by the verb value, which is specified while creating the item in QueryContextMenu. Verb is a string with the command name that is executed by the menu item. Verb is given in two versions: with and without the use of Unicode. Therefore, the verb format should be checked before executing a command.
The GetCommandString method is used to obtain a canonical verb name assigned to the menu item. This method is optional and set for Windows XP and earlier versions of Windows. It’s used to display the hint text for the selected menu item in the status bar.
IShellIconOverlayIdentifier
This interface must be implemented by the overlay icon handler:
C#
class IShellIconOverlayIdentifier: public IUnknown
{
public:
STDMETHOD(GetOverlayInfo)(LPWSTR pwszIconFile, int cchMax, int *pIndex, DWORD* pdw-Flags) = 0;
STDMETHOD(GetPriority)(int* pPriority) = 0;
STDMETHOD(IsMemberOf)(LPCWSTR pwszPath, DWORD dwAttrib) = 0;
};
The GetOverlayInfo method is used for the first call to the overlay handler in order to add an icon to the system’s list of images. After that, the icon can’t be changed and subsequent calls to GetOverlayInfo must return the path of the file containing the icon and the icon index. The icon index is the serial number of an icon (starting with 0) in the DLL resource list.
If the method returns an error for the first run, the icon will not be loaded, even if subsequent calls to GetOverlayInfo are successful. This can lead to a situation when the handler is working and the icon is not displayed.
The GetPriority method is used to specify the overlay icon’s priority over other icons. Depending on it, the handler can either be a top priority or not. The highest priority is 0.
The IsMemberOf method must contain logic that indicates whether to apply the icon to the given object in Windows Explorer. This method is called in turn for all objects in the current Windows Explorer window when the window is opened and after it’s updated. If you don’t specify any filters here and just return S_OK, the icon will be displayed on all objects in Windows Explorer.
Registering Shell extensions
The logic for registering Windows Shell extensions must be implemented in the DllRegisterServer function. This function is called when installing a Shell extension and is responsible for COM object registration. Accordingly, the DllUnregisterServer function must contain code for deleting records from the registry.
Note that you may receive an 0x80070005 error when you try to register a DLL on Windows XP or Windows Server 2003. This happens because their strict security schemes allow only admins to initiate DLL registration.
The registry hive, which contains information about extensions, differs depending on the type of extension handler. For example, for the shortcut menu handler, the path is as follows:
C#
HKEY_CLASSES_ROOT\<file_type>\shellex\ContextMenuHandlers\ExtensionName
The <file_type> parameter may be defined:
- For a specific file type: BMP, DOCX, PNG
- For all file types: *
- For a group of objects: Drive, Directory, Folder, LibraryLocation
The shortcut menu handler must be registered separately for each object type.
The icon overlay handler is registered once using the path:
C#
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\ExtensionName
In addition, you need to register the COM component itself:
C#
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\ExtensionName
For example, the DllRegisterServer function, which registers overlay icon handlers and shortcut menu handlers for all file types (“*”), might look like this:
C#
// GUIDs for COM objects
const CLSID kMenuExtCLSID = { 0xa1239638, 0xb2ba, 0xa2c8, { 0xa4, 0x3b, 0xf6, 0xe2, 0x10, 0x81, 0xf, 0x77 } };
const CLSID kOverlayExtCLSID = { 0xa4569638, 0xc2ba, 0x42c8, { 0xb4, 0xfb, 0xc6, 0xe6, 0x10, 0x91, 0xf, 0x23 } };
STDAPI DllRegisterServer(void)
{
HRESULT hr;
wchar_t szModule[MAX_PATH];
if (GetModuleFileName(g_Dll->DllInst(), szModule, ARRAYSIZE(szModule)) == 0)
{
hr = HRESULT_FROM_WIN32(GetLastError());
return hr;
}
// register COM object for shortcut menu handler
hr = RegisterInprocServer(szModule, kMenuExtCLSID, L"AppCustomExtensions", L"Apartment");
if (SUCCEEDED(hr))
{
// register shortcut menu handler
hr = RegisterShellExtContextMenuHandler(L"*", kMenuExtCLSID, L"ContextMenuExt");
}
// register COM object for overlay icon handler
hr = RegisterInprocServer(szModule, kOverlayExtCLSID, L"AppCustomExtensions", L"Apartment");
if (SUCCEEDED(hr))
{
// register overlay icon handler
hr = RegisterShellExtOverlayIconHandler(kOverlayExtCLSID, L"OverlayIconExt");
}
return hr;
}
The RegisterInprocServer, RegisterShellExtContextMenuHandler, and RegisterShellExtOverlayIconHandler functions contain the code for adding new hives and keys to the registry.
Preparing resources for icon overlay handlers
Icons must be prepared to be correctly displayed by Shell extension handlers.
For the context menu, we need a BMP image. Overlay handlers are a bit more complicated: the icon must be in ICO format and support the following dimensions:
- 10×10 (for 16×16)
- 16×16 (for 32×32)
- 24×24 (for 48×48)
- 128×128 (for 256×256)
Overlay handler icons will be displayed in the lower left corner of the main icon and should take up no more than 25% of the file icon (except for 10×10 for 16×16).
When scaling objects in Windows Explorer, the right size will be determined authomatically.
Installing Shell extensions
Shell extensions are installed using the standard Regsvr32 utility. For example:
ShellScript
> regsvr32 C:\Example\ShellExtensions.dll
When you run this command, the DllRegisterServer function is called, and this function provides extension registration in the registry. All the logic for adding and removing entries from the registry must be implemented by the developer.
After this, the shortcut menu handler immediately begins its work, which can be verified by opening a menu. As for the overlay handler, the icons are not going to appear immediately after registration. In order to download the required resources, you must relaunch Windows Explorer.
Removing Shell extensions
For removing a Shell extension, you can use the same approach used for its installation — namely, Regsvr32:
ShellScript
> regsvr32 /u C:\Exampe\ShellExtensions.dll
Meanwhile, when we call the DllUnregisterServer function, the system deletes records from the registry.
After removing the overlay handler, you’ll also need to relaunch Windows Explorer.
Common problems and their solutions
You may face some Windows Shell extension problems when writing Shell extension handlers. Some of these problems are listed below together with ways to overcome them:
1. Overlay icons are not displayed
The problem: As described above, this problem may be in the logic of the GetOverlayInfo function. Also, as Windows uses only a limited number of registered overlay icon handlers, all other handlers are simply ignored.
A list of the handlers that won’t be ignored is determined by the name in the registry branch: HKEY_LOCAL_MACHINE Software Microsoft Windows CurrentVersion Explorer ShellIconOverlayIdentifiers. The system takes the first 15 overlay icon handlers in alphabetical order. Therefore, if Dropbox or TortoiseSVN, which record a number of extensions, are installed in the system, they can become a reason for not displaying icons.
How to solve this: Add any character before the overlay handler name during registration. This will make the extension appear at the top of the list of extensions. For example, you can add spaces before the handler name.
2. Issues with displaying icons with transparent backgrounds in the context menu
The problem is in using a BMP image that does not support transparency while you add a new menu item. If you use ICO or PNG files, then the icon will be displayed on a black background after its conversion.
How to solve this: Redraw the ICO to BMP format in the code while the extension is running. For example, the following implementation can be used (with UX-THEME.DLL):
C#
m_beginPainFunc = (BeginPaintFunction)::GetProcAddress(m_hUxTheme,
"BeginBufferedPaint");
m_endPaintFunc = (EndPaintFunction)::GetProcAddress(m_hUxTheme,
"EndBufferedPaint");
...
SIZE iconSize;
RECT iconRect;
HBITMAP hBmp = NULL;
HDC hdcDest = CreateCompatibleDC(NULL);
// set the icon size
iconSize.cx = GetSystemMetrics(SM_CXSMICON);
iconSize.cy = GetSystemMetrics(SM_CYSMICON);
SetRect(&iconRect, 0, 0, iconSize.cx, iconSize.cy);
if (hdcDest)
{
if (SUCCEEDED(CreateHbitmap(hdcDest, &iconSize, NULL, &hBmp)))
{
HBITMAP oldBitmap = (HBITMAP)SelectObject(hdcDest, hBmp);
if (oldBitmap)
{
// set the necessary parameters
BLENDFUNCTION bfAlpha = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
BP_PAINTPARAMS paintParams = {0};
paintParams.cbSize = sizeof(paintParams);
paintParams.dwFlags = BPPF_ERASE;
paintParams.pBlendFunction = &bfAlpha;
// repaint the icon
HDC hdcBuffer;
HPAINTBUFFER hPaintBuffer = m_beginPainFunc(hdcDest, &iconRect, BPBF_DIB, &paintParams, &hdcBuffer);
if (hPaintBuffer)
{
if (DrawIconEx(hdcBuffer, 0, 0, hIcon, iconSize.cx, iconSize.cy, 0, NULL, DI_NORMAL))
{
ConvertBufferToPARGB32(hPaintBuffer, hdcDest, hIcon, iconSize);
}
m_endPaintFunc(hPaintBuffer, TRUE);
}
SelectObject(hdcDest, oldBitmap);
}
}
DeleteDC(hdcDest);
}
3. Issues with displaying the context menu
There can be several problems associated with the context menu.
The problem: After registering a shell extension for LibraryLocation, the context menu extension is not displayed.
The problem may be in the implementation of the Initialize method in the IShellExtInit interface, which is called to initialize the Shell extension. IdataObject, which is used for receiving objects for which the extension will be displayed, is passed to the Initialize method as a parameter. So when you click on the library folders (Documents, Music, etc.), any attempts to receive the CF_HDROP format objects from it don’t lead to the expected result.
How to solve this: Use the CFSTR_SHELLIDLIST format, which is similar to CF_HDROP but contains PIDL (a pointer to the object ID in the ITEMIDLIST Shell structure) instead of the file path. It allows CFSTR_SHELLIDLIST to process system and virtual file system objects.
C#
Int m_shellIdList = RegisterClipboardFormat(CFSTR_SHELLIDLIST);
IFACEMETHODIMP ContextMenuExt::Initialize(
LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hKeyProgID)
{
if (pDataObj == NULL)
{
return E_INVALIDARG;
}
HRESULT hr = E_FAIL;
// instead of
// FORMATETC etc = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
FORMATETC fe = { (CLIPFORMAT) m_shellIdList, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
. . .
}
The problem: When registering a Shell extension for AllFileSystemObjects, clicking .lnk files adds this Windows Shell extension to the context menu twice.
The problem is that the extension works both for the object and for the link that points to it.
How to solve this: In the QueryContextMenu method of the IContextMenu interface, not only must the CMF_DEFAULTONLY flag be checked but also the context menu extensions should ignore objects with the CMF_NOVERBS and CMF_VERBSONLY flags.
The problem: The context menu extension is not displayed for some types of files, although it can be registered for all types.
The problem may be in the implementation of the QueryContextMenu method of the IcontextMenu interface. In case of successful implementation, it should return HRESULT with a code value equal to the maximum offset of the command identifier that was assigned plus one.
For example, if idCmdFirst equals 5 and 3 more menu items with 5, 7, and 8 command identifiers were added, then the return value should be: MAKE_HRESULT(SEVERITY_SUCCESS, 0, 8 – 5 + 1).
If you forget to add one, the context menu extension will still work but not in all cases.
How to solve it: Make sure the QueryContextMenu method of the IcontextMenu interface is successfully implemented.
4. The problem with starting a thread from DllMain
The problem: When you start a thread in DllMain (for example, to initialize something), there may be problems with DLL unloading using FreeLibrary.
For example, a DLL with Shell extensions loads dynamically in order to unregister extensions manually. At the same time, you start a thread in DllMain to initialize logging. In this case, the DllUnregisterServer is called and successfully unloads the DLL. Then, as you might expect, the system calls FreeLibrary.
In this example, it may happen that a thread called from DllMain either has not completed execution or has not started yet, while a DLL has already been unloaded from the process context. As a result, memory areas are damaged and access violations may occur at any time when there’s a reference to these areas.
How to solve it: Wait for the end of the initialization thread in the exported DLL functions.
Conclusion
In this article, we discussed all types of Windows Shell extensions as well as a general approach to their implementation. In addition, we described the basic steps for creating a shortcut menu and overlay icon handlers. We also examined several pitfalls of implementing these extensions and provided tips on how to overcome them. To get the sample project sources, you can initiate the Windows Shell extension download here.
Apriorit has a team of qualified Windows system engineers and C/C++ expert developers who can assist you with projects of any complexity.
In Windows you can right-click a file to show a context-menu and choose from various options.
There can be customized options for specific file types if you add registry keys to both HKEY_CLASSES_ROOT (or HKEY_LOCAL_MACHINE).
Originally, Gumby wrote such a shell extension but it refuses to work on some systems. Either Windows 10 needs HKEY_LOCAL_MACHINE as well or other registry settings of other programs are interfering.
Setup for oni. file type
HKEY_CLASSES_ROOT
Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT\.oni] [HKEY_CLASSES_ROOT\.oni\shell] [HKEY_CLASSES_ROOT\.oni\shell\OniSplit_or_OniSplitGUI] @="Command name to be displayed in context menu" [HKEY_CLASSES_ROOT\.oni\shell\OniSplit_or_OniSplitGUI\command] @="\"C:\\PATH\\Batch_for_OniSplit_or_OniSplitGUI.exe\" \"%1\""
HKEY_LOCAL_MACHINE
Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Classes\.oni] [HKEY_LOCAL_MACHINE\SOFTWARE\Classes\.oni\shell] [HKEY_LOCAL_MACHINE\SOFTWARE\Classes\.oni\shell\OniSplit_or_OniSplitGUI] @="Command name to be displayed in context menu" [HKEY_LOCAL_MACHINE\SOFTWARE\Classes\.oni\shell\OniSplit_or_OniSplitGUI\command] @="\"C:\\PATH\\Batch_for_OniSplit_or_OniSplitGUI.exe\" \"%1\""
Setup for .xml file type
Since .xml is a widly used file extension there seem to be additional settings had override the default lookups for better management.
The .xml key at HKEY_LOCAL_MACHINE\SOFTWARE\Classes has a Default value (redirect) with «xmlfile«.
If you remove the xmlfile value then Classes\xmlfile will be looked up nonetheless. (Doublecheck on other machines.)
So, next reg path to modify is this:
[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\xmlfile\shell\OniSplit_or_OniSplitGUI] [HKEY_LOCAL_MACHINE\SOFTWARE\Classes\xmlfile\shell\OniSplit_or_OniSplitGUI\command] @="\"C:\\PATH\\Batch_for_OniSplit_or_OniSplitGUI.exe\" \"%1\""
Notes for repair after standard application changed
When you change xml standard application following gets added and breaks the context menu options:
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.xml\UserChoice]
As soon as you remove this key context menu works again. However this removes the possibility to double-click .xml to open them.
Somthing is still missing.
Advanced conversions
If another program handles OniSplit and automatically feeds it with the right arguments you would only need one explorer context menu option such as:
add to OniSplit helper list and process
Some conversions require more than one input file. In that case can collect the other files first:
add to OniSplit helper list
Additional, useful options might be:
repeat last OniSplit helper conversion clear OniSplit helper list
Install and uninstall
A less tech-savvy modder shouldn’t need to deal with registry settings.
AEI could be used to distribute an OniSplitHelper tool that manages the registry.
Perhaps «Vago» and «Simple Onisplit GUI» could be adapted to do that.
Download Windows Speedup Tool to fix errors and make PC run faster
We have seen where the Windows Registry is located physically on the disk and how we can access and edit it using the Registry Editor. However, if you wish, you can access and modify the registry directly through Windows File Explorer using Windows Registry shell extension.
The Windows Registry is a directory that stores settings and options for Microsoft Windows’s operating system. It contains information and settings for all the hardware, operating system software, most non-operating system software, users, and PC preferences.
Windows Registry shell extension
Download and install Windows Registry shell extension.
It is a Shell Namespace Extension that allows you to browse and edit the Windows Registry from the Computer folder.
It creates a virtual folder in the (My) Computer/This PC folder and populates the Windows Registry branch.
This tool allows editing the Windows Registry. This is a potentially hazardous operation that could render your system unbootable if done incorrectly. If this happens you will have to restore a registry backup or reinstall Windows.
Check it out and download it from its home page. Do not install this tool for editing the Windows Registry unless you know what you are doing.
TIP: How to edit the Windows Registry without opening regedit.exe but by using Console Registry Tool or reg.exe, may also interest you.
Anand Khanse is the Admin of TheWindowsClub.com, a 10-year Microsoft MVP (2006-16) & a Windows Insider MVP (2016-2022). Please read the entire post & the comments first, create a System Restore Point before making any changes to your system & be careful about any 3rd-party offers while installing freeware.
Время на прочтение9 мин
Количество просмотров20K
Для повышения удобства разрабатываемых продуктов, мы стараемся обеспечить максимальный уровень интеграции функционала в операционную систему, чтобы пользователю было удобно использовать весь потенциал приложения. В этой статье будут рассмотрены теоретические и практические аспекты разработки Shell Extensions, компонентов позволяющих интегрироваться в оболочку операционной системы Windows. В качестве примера рассмотрим расширение списка контекстного меню для файлов, а так же проведем обзор уже существующих решений в этой области.
Вообще говоря, существует огромное множество вариантов интеграционных компонентов оболочки операционной системы Windows, например: апплеты панели управления, хранители экрана и прочее, однако в данной статье мне бы хотелось подробнее остановиться на возможностях расширения Windows Explorer, компонента ОС, который мне приходилось расширять больше остальных, ввиду его функциональной нагрузки.
Windows Explorer позволяет расширять себя посредством использования специализированных COM объектов. Windows API, содержит интерфейсы и структуры описывающие, как должны работать такие COM объекты, какие методы должны экспортировать. После того как COM объект реализующий нужный функционал разработан, он регистрируется по определенному пути в реестре Windows, так чтобы Windows Explorer при выполнении описанного при регистрации функционала обратился к соответствующему COM объекту.
Итак, начнем разработку COM объекта позволяющего расширить список контекстного меню для файлов.
Разрабатывать будем при помощи .net framework.
Добавим в Assembly.cs следующие директивы, позволяющие использовать нашу сборку как COM объект:
// 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")]
Нам понадобиться импортировать некоторые функции 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);
Функция DragQueryFile позволит нам получить список файлов выбранных в каталоге, SHChangeNotify оповестить операционную систему о том, что оболочка была изменена.
Так как мы разрабатываем COM объект, который расширяет контекстное меню, мы должны реализовать интерфейс IShellExtInit. В методе Initialize мы получим базовую информацию о каталоге, в котором выполняемся.
[ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), GuidAttribute("000214e8-0000-0000-c000-000000000046")]
public interface IShellExtInit
{
[PreserveSig()]
int Initialize (IntPtr pidlFolder, IntPtr lpdobj, uint /*HKEY*/ hKeyProgID);
}
Также необходимо описать и реализовать COM интерфейс IContextMenu. Значение поля PreserveSig, равное true, инициирует непосредственное преобразование неуправляемых сигнатур со значениями HRESULT или retval, а значение false вызывает автоматическое преобразование значений HRESULT или retval в исключения. По умолчанию для поля PreserveSig используется значение 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);
}
Метод QueryContextMenu будет вызван при вызове контекстного меню, в нем нам будет нужно реализовать функционал по добавлению пункта меню, GetCommandString будет возвращать некоторые детали по данной команде, ее описание и прочее. InvokeCommand будет вызван при выборе пункта меню, который мы добавим.
Для COM объекта так же необходимо реализовать функции установки и удаления.
[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);
}
}
В этой функции мы выполняем регистрацию нашего компонента в реестре, так как у нас компонент расширяющий функционал контекстного меню, мы регистрируемся в разделе ContextMenuHandlers (*\\shellex\\ContextMenuHandlers\\ExShell). После регистрации перезапускаем процесс explorer.exe для того, чтобы наши изменения сразу вступили в силу.
[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);
}
}
Функция удаления компонента, очищаем реестр от созданных ранее ключей.
Далее переходим к процессу реализации интерфейсных функций.Реализуем интерфейсы IShellExtInit, IContextMenu. Не буду детально описывать весь код этого класса, остановлюсь на реализации функций данных интерфейсов.
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;
}
Функция инициализации компонента, запускается при открытии каталога или любого другого объекта, в контексте которого может находиться контекстное меню. Используем интерфейс IDataObject для получения данных о текущем объекте, в частности нас интересует hGlobal. Этот Handle идентифицирует текущий объект, внутри которого и происходит наше выполнение.
Далее рассмотрим функцию, которая вызывается при выпадении контекстного меню.
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;
В этом участке кода проверяем, что выполняемся в нужном контексте и пытаемся запросить количество выбранных в каталоге файлов, а так же сохранить список этих файлов. Также замечу, что при передаче iFile = 0xffffffff в функцию DragQueryFile она вернет количество файлов в каталоге.
// 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;
}
Здесь добавляем новый пункт меню при помощи вызова функции InsertMenuItem, затем подготавливаем и добавляем иконку к этому пункту меню, а так же разделительную линию для эстетической красоты. Структура MENUITEMINFO описывает наш пункт меню, а именно его тип (ftype), содержащиеся данные (dwTypeData), состояние (fState), идентификатор пункта меню (wID). Переменная hMenu идентифицирует текущее выпавшее меню, iMenu позиция по которой мы добавляемся. Для того, чтобы получить более полную информацию, можно обратиться в MSDN.
Далее рассмотрим функцию GetCommandString
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;
}
}
Данная функция возвращает независимое от языка описание команды, а так же краткую подсказку в виде helptext соответственно.
Ну и последняя функция, которая будет вызвана при выборе нашего пункта меню:
void IContextMenu.InvokeCommand (IntPtr pici)
{
try
{
System.Windows.Forms.MessageBox.Show("Test code");
}
catch(Exception exe)
{
EventLog.WriteEntry("Application", exe.ToString());
}
}
Тут все достаточно прозрачно.
Так как наш COM объект выполняется в контексте Windows Explorer, то для того чтобы его отлаживать, нам нужно подключаться к процессу explorer.exe. Для регистрации и удаления компонента Вы можете использовать bat файлы, поставляемые с исходниками к данной статье. Для регистрации мы используем RegAsm.exe утилиту, позволяющую COM клиентам использовать .net класс будто бы это COM, а также GacUtil для того, чтобы поместить сборку в GAC. После регистрации процесс explorer.exe будет перезапущен.
Так же обращу Ваше внимание на утилиту позволяющую просмотреть, а при необходимости и отредактировать все установленные в системе расширения Windows Explorer. Утилита называется ShellExView, скачать можно на сайте производителя Nirsoft или в приложении к статье.
Вот так выглядит наш компонент в ShellExView:
Так он выглядит при раскрытом контекстном меню:
Итак, мы рассмотрели пример разработки компонента расширяющего Windows Explorer, но данный вид расширения далеко не единственное, что мы можем изменять, и на что можем влиять.
Имея понимание того, как функционируют такого рода компоненты можно взглянуть на то, что уже было разработано сообществом и может быть использовано для облегчения выполнения таких операций.
Например, библиотека SharpShell позволяющая выполнять достаточно большой объем модификаций, а так же хорошо описанная в цикле статей .NET Shell Extensions на CodeProject. В качестве аналога также можно использовать библиотеку Windows Shell Framework или библиотеку для связки ATL + С++ Mini Shell Extension Framework.
Также обращу Ваше внимание на предупреждение Microsoft относительно разработки таких вот расширений: “Не рекомендуется писать расширения оболочки в языках .NET, так как используется только одна среда выполнения CLR для одного процесса, поэтому может возникнуть конфликт между двумя расширениями оболочки, использующими разные версии CLR. Однако .Net Framework 4 поддерживает технологию side-by-side для версий .Net Framework 2.0, 3.0, 3.5 и позволяет в одном и том же процессе использовать как старую CLR 2, так и новую CLR 4”.
Подробнее почитать про ограничения использования .net при разработке Shell Extensions можно здесь: Guidance for Implementing In-Process Extensions
Еще посмотреть:
Explorer column handler shell extension in C#
Creating Shell Extension Handlers
Исходники и утилиты: ExampleShell.rar
P.S Расширение не тестировал на Windows 8, судя по отзывам, для корректной работы в реестре нужно установить в разделе HKEY_CLASSES_ROOT\CLSID\{guid компонента}\InprocServer32 следующее значение ThreadingModel = Apartment.
Спасибо за внимание!
На этой страничке собраны мои и не мои расширения оболочки Windows (Windows shell extensions), т.е. Проводника Windows.
Кроме исходных текстов здесь находятся собранные дистрибутивы, так что можно как заниматься самообразованием, так и просто пользоваться представленными программами.
На сайте RSDN находится хороший перевод на руский язык цикла статей:
Руководство полного идиота по написанию расширений оболочки.
DllReg
#
Расширение контекстного меню (context menu shell extension), добавляющего два пункта “Register server(s)” (Регистрация сервера) и “UnRegister servers(s)” (Дерегистрация сервера) для файлов с расширением DLL, OCX, AX, являющихся COM-серверами.
eShell
#
Расширение свойств (properties shell extension) и дополнительных значков (overlay icon shell extension) папок, предназначенное для управления виртуальными папками веб-сервера EServ. Собственно требуется установленный EServ 2.x.
MiniPicture
#
Расширение контекстного меню (context menu shell extension), добавляющего маленькую картинку (thumbnail) для графических файлов форматов JPG, GIF, BMP, WMF и др. поддерживаемых текущей установленной версией Internet Explorer ™, т.к. для загрузки картинки используется стандартная библиотека работы с изображениями Internet Explorer.
SendToClone
#
Расширение бросателя файлов (drop handler shell extension), добавляющего пункт “Send To → Some other folder” предназначенного для копирования или перемещения файлов.
