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:

  1. We need to measure & draw menu in all stated ourselves, which makes us write plenty of code.

  2. 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.

  3. We need to keep extra context information for each menu item with text, icon handle, etc.

  4. 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:

  1. 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 2000 shell.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 menus HBMMENU_CALLBACK method can be used without any problem. So you may not care about it unless you are shell extension developer.

  2. Vista is removing menu theme when some menu item has hbmpItem set to HBMMENU_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:

  1. srcTortoiseShellContextMenu.cpp,
  2. srcTortoiseShellShellExt.cpp
  3. 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.