Themed menu’s icons, a complete Vista and XP solution (updated)
Update: Steve King has patched my Vista GDI+ based menus with pure GDI method at Tortoise SVN revision 14191 as described lately by Microsoft. Pure GDI method no longer requires GDI+, which is not present in Premium versions of Vista, maintaining full compatibility with older versions of Windows.
I’m an author of few patches for both Tortoise SVN and Tortoise
CVS that makes them display the explorer’s context menu icons nicely on
XP and Windows 2000. Both programs are implementing IContextMenu and using
QueryContextMenu function to create items of popup menu of explorer. Briefly
the called extension must fill menu items with InsertMenuItem using supplied
HMENU
hmenu
parameter.
During development of those few patches I’ve learnt some few new things about way we make icons displayed next to menu items I want to share with you.
How to get icons in context menus
Methods described here are related to shell context menu extension, however they can be used in any Windows application.
hbmp(Un)checked method
Old Tortoise CVS menu iconsInitially both Tortoises were filling
hbmpUnchecked
& hbmpChecked
fields of MENUITEMINFO
that is passed to
InsertMenuItem with HBITMAP
created from icon to get icons on menu item. This
solution works on all Windows since 95. However the strong limitation is that
HBITMAP
must be SM_CXMENUCHECK
x SM_CYMENUCHECK
(usually 12 x 12). So if
you are using 16 x 16 icon, the icon gets squished and looks awfully. The
function used to convert icon to bitmap is:
HBITMAP CShellExt::IconToBitmap(std::string sIcon)
{
RECT rect;
rect.right = ::GetSystemMetrics(SM_CXMENUCHECK);
rect.bottom = ::GetSystemMetrics(SM_CYMENUCHECK);
rect.left = rect.top = 0;
HICON hIcon = (HICON)LoadImageA(g_hInstance, sIcon.c_str(), IMAGE_ICON,
rect.right, rect.bottom, LR_DEFAULTCOLOR);
if (!hIcon)
return NULL;
HWND desktop = ::GetDesktopWindow();
if (desktop == NULL)
{
DestroyIcon(hIcon);
return NULL;
}
HDC screen_dev = ::GetDC(desktop);
if (screen_dev == NULL)
{
DestroyIcon(hIcon);
return NULL;
}
// Create a compatible DC
HDC dst_hdc = ::CreateCompatibleDC(screen_dev);
if (dst_hdc == NULL)
{
DestroyIcon(hIcon);
::ReleaseDC(desktop, screen_dev);
return NULL;
}
// Create a new bitmap of icon size
HBITMAP bmp = ::CreateCompatibleBitmap(screen_dev, rect.right, rect.bottom);
if (bmp == NULL)
{
DestroyIcon(hIcon);
::DeleteDC(dst_hdc);
::ReleaseDC(desktop, screen_dev);
return NULL;
}
// Select it into the compatible DC
HBITMAP old_dst_bmp = (HBITMAP)::SelectObject(dst_hdc, bmp);
if (old_dst_bmp == NULL)
{
DestroyIcon(hIcon);
return NULL;
}
// Fill the background of the compatible DC with the given colour
::SetBkColor(dst_hdc, RGB(255, 255, 255));
::ExtTextOut(dst_hdc, 0, 0, ETO_OPAQUE, &rect, NULL, 0, NULL);
// Draw the icon into the compatible DC
::DrawIconEx(dst_hdc, 0, 0, hIcon, rect.right, rect.bottom, 0, NULL, DI_NORMAL);
// Restore settings
::SelectObject(dst_hdc, old_dst_bmp);
::DeleteDC(dst_hdc);
::ReleaseDC(desktop, screen_dev);
DestroyIcon(hIcon);
return bmp;
}
Ownerdraw method
Tortoise SVN was using also owner draw method. I won’t describe here details of this method. This relays on MENUITEMINFO fType flag set to MFT_OWNERDRAW. Shell extension in HandleMenuMsg2 callback should handle WM_MEASUREITEM and WM_DRAWITEM. This method is generally OK, however it has several flaws:
We need to measure & draw menu in all stated ourselves, which makes us write plenty of code.
Ownerdraw menus are not respecting visual styles of Windows XP or Vista. We would need to use uxtheme functions to somehow handle rendering of menu parts on those systems.
We need to keep extra context information for each menu item with text, icon handle, etc.
Keyboard shortcuts doesn’t work automatically, we must handle
WM_MENUCHAR
to make them work.
HBMMENU_CALLBACK method
Since Windows 98 MENUITEMINFO
has extra field hbmpItem
. This field can be
used for setting the HBITMAP with bitmap that is displayed next to the menu
item. hbmpItem
can be set also to HBMMENU_CALLBACK
which will make menu item
work like owner-draw, but WM_MEASUREITEM & WM_DRAWITEM just need to handle icon
drawing, rest will be done by Windows. This method is easiest to implement
and so it is used inside many application, I just name on I use or develop:
wxWidgets SDK, Miranda IM. We just need to initialize menu item like that:
MENUITEMINFO menuiteminfo;
ZeroMemory(&menuiteminfo, sizeof(menuiteminfo));
menuiteminfo.cbSize = sizeof(menuiteminfo);
menuiteminfo.fMask = MIIM_FTYPE | MIIM_ID | MIIM_SUBMENU | MIIM_DATA | MIIM_BITMAP | MIIM_STRING;
menuiteminfo.fType = MFT_STRING;
menuiteminfo.dwTypeData = lpszMenuTitle;
menuiteminfo.cch = _tcslen(lpszMenuTitle);
menuiteminfo.hbmpItem = HBMMENU_CALLBACK;
menuiteminfo.wID = id;
Rest is done in WM_MEASUREITEM where we need to just make sure we have space for 16 x 16 image using:
case WM_MEASUREITEM:
{
MEASUREITEMSTRUCT* lpmis = (MEASUREITEMSTRUCT*)lParam;
if (lpmis==NULL)
break;
lpmis->itemWidth += 2;
if (lpmis->itemHeight < 16)
lpmis->itemHeight = 16;
*pResult = TRUE;
}
break;
Then to draw an icon we need to handle WM_DRAWITEM, but just drawing the icon, nothing else:
case WM_DRAWITEM:
{
LPCTSTR resource;
DRAWITEMSTRUCT* lpdis = (DRAWITEMSTRUCT*)lParam;
if ((lpdis==NULL)||(lpdis->CtlType != ODT_MENU))
return S_OK; // not for a menu
resource = GetMenuIconResourceID(lpdis->itemID);
if (resource == NULL)
return S_OK;
HICON hIcon = (HICON)LoadImage(g_hResInst, resource, IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR);
if (hIcon == NULL)
return S_OK;
DrawIconEx(lpdis->hDC,
lpdis->rcItem.left - 16,
lpdis->rcItem.top + (lpdis->rcItem.bottom - lpdis->rcItem.top - 16) / 2,
hIcon, 16, 16,
0, NULL, DI_NORMAL);
DestroyIcon(hIcon);
*pResult = TRUE;
}
break;
Simple ? Yes it is. However there are some issues with this method as well:
When this method is used on Windows 2000 shell extension window background popup menu, then
shell.dll
is removing text from the menu, so we see just icons.This is obviously a bug of Windows 2000shell.dll
, because MSDN documentation states this shall work regardless of the sittuation, but we need to somehow get over it. Surprisingly it does work fine when we right-click on explorer item (file or folder). The easiest solution is using hbmp(Un)checked method when uFlags == 0 of QueryContextMenu, which indicated we clicked the background, so we fall back to most primitive method, but at least we got text and “some” icons in the menu.Note: This bug only appears in Windows 2000 explorer’s background menu of shell extension, so in every other situation as standalone program menusHBMMENU_CALLBACK
method can be used without any problem. So you may not care about it unless you are shell extension developer.Vista is removing menu theme when some menu item has
hbmpItem
set toHBMMENU_CALLBACK
, so we will have nice icons and nice menu, but if we want to have nice icons with 100% themed menu on Vista we need to use last method.
Vista PARGB32 hbmpItem bitmap method (Updated)
Vista strongly relays on 32-bit pre-multiplied alpha RGB bitmaps for rendering
its interface. In Vista hbmpItem
can be set to PARGB32 HBITMAP
and this
bitmap will be nicely displayed by Vista together with theming as you can see
on the screenshot at right. I got know of this possibility reading nice article
Vista Style Menus, Part 1 – Adding icons to standard menus at ShellRevealed
blog.
The most important question is how to we get our icon (regardless it is 32-bit
with alpha, or 256-color with mask) converted to PARGB32 HBITMAP
. Windows API
doesn’t give such possibility straight of the box. Article from ShellRevealed
proposes WIC (Windows Imaging Component) which is cool & quite simple for
conversion or Vista’s GDI method, but those require Vista SDK, which may be
annoying for those using *Visual Studio*‘s out of the box.
As an alternative to that I’ve used Gdiplus which is present on most of the systems since Windows 2000, and most shipped with *Visual Studio*s Platforms SDKs. This method is also much simpler than WIC or GDI method from described article.
Alternative solution to WIC is to use pure Vista GDI (UxTheme) calls as
described at MSDN (GDI_CVistaMenuApp.cpp
sample). It was implemented
in Tortoise SVN revision 14191 by Steve King. It uses
BeginBufferedPaint
, EndBufferedPaint
and GetBufferedPaintBits
dynamically
loaded from Vista‘s UXTHEME.DLL
and Create32BitHBITMAP
and
ConvertBufferToPARGB32
from GDI_CVistaMenuApp.cpp
sample.
The PARGB32 function is as follows:
HBITMAP CShellExt::IconToBitmapPARGB32(std::string sIcon)
{
HRESULT hr = E_OUTOFMEMORY;
HBITMAP hBmp = NULL;
HICON hIcon = (HICON)LoadImageA(g_hResInst, sIcon.c_str(), IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR);
if(!hIcon)
return NULL;
SIZE sizIcon;
sizIcon.cx = GetSystemMetrics(SM_CXSMICON);
sizIcon.cy = GetSystemMetrics(SM_CYSMICON);
RECT rcIcon;
SetRect(&rcIcon, 0, 0, sizIcon.cx, sizIcon.cy);
HDC hdcDest = CreateCompatibleDC(NULL);
if(hdcDest) {
hr = Create32BitHBITMAP(hdcDest, &sizIcon, NULL, &hbmp);
if(SUCCEEDED(hr)) {
hr = E_FAIL;
HBITMAP hbmpOld = (HBITMAP)SelectObject(hdcDest, hbmp);
if(hbmpOld) {
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;
HDC hdcBuffer;
HPAINTBUFFER hPaintBuffer = pfnBeginBufferedPaint(hdcDest, &rcIcon, BPBF_DIB, &paintParams, &hdcBuffer);
if(hPaintBuffer) {
if(DrawIconEx(hdcBuffer, 0, 0, hIcon, sizIcon.cx, sizIcon.cy, 0, NULL, DI_NORMAL)) {
// If icon did not have an alpha channel, we need to convert buffer to PARGB.
hr = ConvertBufferToPARGB32(hPaintBuffer, hdcDest, hIcon, sizIcon);
}
// This will write the buffer contents to the destination bitmap.
pfnEndBufferedPaint(hPaintBuffer, TRUE);
}
SelectObject(hdcDest, hbmpOld);
}
}
DeleteDC(hdcDest);
}
DestroyIcon(hIcon);
if(SUCCEEDED(hr)) {
return hBmp;
}
DeleteObject(hBmp);
return NULL;
}
So in this case instead:
menuiteminfo.hbmpItem = HBMMENU_CALLBACK
we do:
menuiteminfo.hbmpItem = IconToBitmapPARGB32(lpszIconResourceID)
We shouldn’t forget of initializing Gdiplus library with:
GdiplusStartup(&m_gdipToken, &gdiplusStartupInput, NULL);
in program/DLL initialization code and shutting it down after all with
GdiplusShutdown(m_gdipToken)
.
If we want to be compatible with older Windows versions, we shall load (map)
the Vista‘s UXTHEME.DLL
functions dynamically only on Vista:
typedef DWORD ARGB;
typedef HRESULT (WINAPI *FN_GetBufferedPaintBits) (HPAINTBUFFER hBufferedPaint, RGBQUAD **ppbBuffer, int *pcxRow);
typedef HPAINTBUFFER (WINAPI *FN_BeginBufferedPaint) (HDC hdcTarget, const RECT *prcTarget,
BP_BUFFERFORMAT dwFormat, BP_PAINTPARAMS *pPaintParams, HDC *phdc);
typedef HRESULT (WINAPI *FN_EndBufferedPaint) (HPAINTBUFFER hBufferedPaint, BOOL fUpdateTarget);
/* (...) */
HMODULE hUxTheme = ::GetModuleHandle (_T("UXTHEME.DLL"));
pfnGetBufferedPaintBits = (FN_GetBufferedPaintBits)::GetProcAddress(hUxTheme, "GetBufferedPaintBits");
pfnBeginBufferedPaint = (FN_BeginBufferedPaint)::GetProcAddress(hUxTheme, "BeginBufferedPaint");
pfnEndBufferedPaint = (FN_EndBufferedPaint)::GetProcAddress(hUxTheme, "EndBufferedPaint");
Since we are going to use Gdiplus only on Vista, we may use Gdiplus.dll
as
delayed load DLL, so it won’t be loaded on older systems using previous
methods, saving us some memory. Simple enough ?
Testing Windows version number with GetVersionEx
and combining this method
for Vista with HBMENU_CALLBACK
method for Windows XP and older systems
(with hbmp(Un)checked fallback on explorer extension on Windows 2000 if
needed) is my opinion best method of having nice menus in all modern Windows
systems. This is also current display method of Tortoise CVS &
Tortoise SVN latest development versions. If you need full code to
browse you may want look into Tortoise SVN SVN trunk files:
srcTortoiseShellContextMenu.cpp
,srcTortoiseShellShellExt.cpp
srcTortoiseShellShellExt.h
.
Conclusion
All those hacks and recipes would be worthless if only there was simple consistent API for making menu item icons. Unfortunately menu icons, something that was always present in Windows and *Microsoft * applications, never got any decent API, moreover the methods to get those icons working change for every major Windows release, making us developers wasting our time “porting” our applications to new “shinny” Windows rather than doing something productive.
One thing that is simply unacceptable for me (even more since I now work
regularly on OSX) is that Windows system apps and Microsoft regular
applications are using so many UI hacks and mods that are never exposed to the
developers trough API. Those are either closed libraries like one for Office’s
or Visual Studio GUI, or Vista hacks with PARGB that require tricky in memory
conversions rather than just pointing hbmpItem
to HICON
and making Vista to
do the conversion on its own.