Saturday, 17 August 2019

How to assign a icon for "Copy/Cut/Paste/Delete" Windows default context menu items?


Under Windows 8/8.1 x64, I would like to assign a custom icon for the default Windows context menu items such as Copy, Cut, Paste, Delete, Undo, Redo and Send To items, which by default has any icon:


enter image description here


Where I can locate the "reference" to those context menu items in the registry then add a "icon" registry value for them?


Or in other words, how to assign a icon to a shell extension menu like the SendTo shellex?.


Research




As commented by @Sk8erPeter, seems that:



"Adding the Icon string value to different context menu handlers doesn't work like when adding it to a custom item like e.g. HKEY_CLASSES_ROOT\*\shell\MYCUSTOMKEY"




Answer



Affiliation notice: I am the author of the software mentioned in this answer.


First up, I'll have you know that I learned C++ and Win32 just for this question.


I have developed a 64-bit shell extension that gets registered as a context menu handler. When it's invoked, it rummages through the existing menu items, looking for interesting entries. If it finds one, it sticks an icon on it (which must have been loaded earlier). At the moment, it looks for Copy, Cut, Delete, Paste, Redo, Send to, and Undo. You can add your own by modifying the code; the procedure for this is described below. (Sorry, I'm not good enough at C++ to make it configurable.)


A screenshot of it in action, with the ugliest icons known to man:


in action


You can download these icons if you really want to.


Setting it up


Download it (from my Dropbox). Notice: this file is detected by one VirusTotal scanner as being some form of malware. This is understandable, given the kind of things it has to do to whack the existing entries. I give you my word that it does no intentional harm to your computer. If you're suspicious and/or you want to modify and extend it, see the code on GitHub!


Create a folder in your C drive: C:\shellicon. Create BMP files with the following titles: copy, cut, delete, paste, redo, sendto, undo. (Hopefully it's obvious which one does which thing.) These images should probably be 16 by 16 pixels (or however big your DPI settings make the menu margin), but I've had success with larger ones as well. If you want the icons to look transparent, you'll have to just make their background the same color as the context menu. (This trick is employed by Dropbox as well.) I made my terrible icons with MS Paint; other programs may or may not save in a manner compatible with LoadImageA. 16 by 16 at 24-bit color depth at 96 pixels per inch seems to be the most reliable set of image properties.


Put the DLL somewhere accessible to all users, that folder you just made is a good choice. Open an admin prompt in the folder containing the DLL and do regsvr32 ContextIcons.dll. This creates registration information for the shell types *, Drive, Directory, and Directory\Background. If you ever want to remove the shell extension, do regsvr32 /u ContextIcons.dll.


Relevant code


Basically, the extension just queries every context menu item's text with GetMenuItemInfo and, if appropriate, adjusts the icon with SetMenuItemInfo.


Visual Studio generates a lot of magic mysterious code for ATL projects, but this is the contents of IconInjector.cpp, which implements the context menu handler:


// IconInjector.cpp : Implementation of CIconInjector

#include "stdafx.h"
#include "IconInjector.h"
#include

// CIconInjector

HBITMAP bmpCopy = NULL;
HBITMAP bmpCut = NULL;
HBITMAP bmpUndo = NULL;
HBITMAP bmpRedo = NULL;
HBITMAP bmpSendto = NULL;
HBITMAP bmpDel = NULL;
HBITMAP bmpPaste = NULL;
STDMETHODIMP CIconInjector::Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hProgID) {
// Load the images
bmpCopy = (HBITMAP)LoadImageA(NULL, "C:\\shellicon\\copy.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE);
bmpCut = (HBITMAP)LoadImageA(NULL, "C:\\shellicon\\cut.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE);
bmpUndo = (HBITMAP)LoadImageA(NULL, "C:\\shellicon\\undo.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE);
bmpRedo = (HBITMAP)LoadImageA(NULL, "C:\\shellicon\\redo.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE);
bmpSendto = (HBITMAP)LoadImageA(NULL, "C:\\shellicon\\sendto.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE);
bmpDel = (HBITMAP)LoadImageA(NULL, "C:\\shellicon\\delete.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE);
bmpPaste = (HBITMAP)LoadImageA(NULL, "C:\\shellicon\\paste.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE);
int err = GetLastError();
return S_OK;
}
STDMETHODIMP CIconInjector::QueryContextMenu(HMENU hmenu, UINT uMenuIndex, UINT uidFirst, UINT uidLast, UINT flags) {
using namespace std;
if (flags & CMF_DEFAULTONLY) return S_OK; // Don't do anything if it's just a double-click
int itemsCount = GetMenuItemCount(hmenu);
for (int i = 0; i < itemsCount; i++) { // Iterate over the menu items
MENUITEMINFO mii;
ZeroMemory(&mii, sizeof(mii));
mii.cbSize = sizeof(mii);
mii.fMask = MIIM_FTYPE | MIIM_STRING;
mii.dwTypeData = NULL;
BOOL ok = GetMenuItemInfo(hmenu, i, TRUE, &mii); // Get the string length
if (mii.fType != MFT_STRING) continue;
UINT size = (mii.cch + 1) * 2; // Allocate enough space
LPWSTR menuTitle = (LPWSTR)malloc(size);
mii.cch = size;
mii.fMask = MIIM_TYPE;
mii.dwTypeData = menuTitle;
ok = GetMenuItemInfo(hmenu, i, TRUE, &mii); // Get the actual string data
mii.fMask = MIIM_BITMAP;
bool chIcon = true;
if (wcscmp(menuTitle, L"&Copy") == 0) {
mii.hbmpItem = bmpCopy;
}
else if (wcscmp(menuTitle, L"Cu&t") == 0) {
mii.hbmpItem = bmpCut;
}
else if (wcscmp(menuTitle, L"&Paste") == 0) {
mii.hbmpItem = bmpPaste;
}
else if (wcscmp(menuTitle, L"Se&nd to") == 0) {
mii.hbmpItem = bmpSendto;
}
else if (wcsstr(menuTitle, L"&Undo") != NULL) {
mii.hbmpItem = bmpUndo;
}
else if (wcsstr(menuTitle, L"&Redo") != NULL) {
mii.hbmpItem = bmpRedo;
}
else if (wcscmp(menuTitle, L"&Delete") == 0) {
mii.hbmpItem = bmpDel;
}
else {
chIcon = false;
}
if (chIcon) SetMenuItemInfo(hmenu, i, TRUE, &mii);
free(menuTitle);
}
return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0); // Same as S_OK (= 0) but is The Right Thing To Do [TM]
}
STDMETHODIMP CIconInjector::InvokeCommand(LPCMINVOKECOMMANDINFO info) {
return S_OK;
}
STDMETHODIMP CIconInjector::GetCommandString(UINT_PTR, UINT, UINT*, LPSTR, UINT) {
return S_OK;
}

Note that the HBITMAPs are never cleaned up, but this doesn't matter too much given that the DLL's stuff will go away when Explorer shuts down. The icons barely take any memory anyway.


If you're compiling for 32-bit, the first parameter to GetCommandString is just a UINT instead of a UINT_PTR.


If you really want transparent icons, you'll have to create a window with the desired icon and then set mii.hBmpItem to HBMMENU_SYSTEM and put the handle to the window in mii.dwItemData, as described at the bottom of the MSDN article on MENUITEMINFO. I wasn't able to figure out how to create windows from shell extensions. LR_LOADTRANSPARENT looks promising as a flag of LoadImageA, but it has its own pitfalls - specifically, not working unless you use 256-color bitmaps.


If you experience problems with image loading, try removing the LR_DEFAULTSIZE flag from the LoadImageA calls.


Somebody sufficiently skilled in C++ could probably grab resources out of other DLLs and convert them to HBITMAPs, but that somebody is not me.


Modifying it


I wrote this in Visual Studio, which I believe to be the best editor for Windows C++.


Load up the SLN file into Visual Studio 2015 after you install the C++ tools. In IconInjector.cpp, you can add HBITMAP entries at the top and LoadImageA calls in Initialize to add new icons. Down in the else if section, use a wcscmp call to look for an exact match, or a wcsstr call to look for the presence of a substring. In both cases, the & represents the position of the underline/accelerator when using Shift+F10. Set your mode to Release and your architecture to x64, and do BuildBuild Solution. You'll get an error about failing to register the output, but don't worry; you'd want to do this manually anyway. End Explorer, copy the new DLL (\x64\Release\ContextIcons.dll in the solution folder) to the place, then do the regsvr32 dance.


Attributions


Many thanks to the MSDN writers, and to the creator of "The Complete Idiot's Guide to Writing Shell Extensions", which I referenced heavily.


Eulogy


To the many Explorer instances that were killed in the production of this shell extension: you died for a great cause, that some people on the Internet can have icons next to their words.


No comments:

Post a Comment

How can I VLOOKUP in multiple Excel documents?

I am trying to VLOOKUP reference data with around 400 seperate Excel files. Is it possible to do this in a quick way rather than doing it m...