Drawing disabled images / icons / buttons

If you've ever needed to draw images or icons as if they are disabled ("grayed out"), you will find the Win32 API to be a disappointment.  DrawState with DST_ICON | DSS_DISABLED works but the images look much worse than the standard disabled images on Vista.  And DrawThemeIcon with PBS_DISABLED does nothing special.

After a quick search, I found a simple function that draws a very nice disabled icon (from here: http://dvinogradov.blogspot.com/2007_01_01_archive.html).  For our needs I had to support transparency for a given color (instead of icons using the alpha channel), so I customized that function to support this form of transparency.

If your source icon is actually using alpha transparency instead of a single-color transparency mechanism, then just skip the FillRect call.  This function could be enhanced to detect that condition, but I'll leave that as an exercise for the reader.

And so I give you DrawDisabledIcon which will treat magenta (RGB(255,0,255)) as the transparent color:

//
// DrawDisabledIcon
//
// DrawState does a terrible job and looks much worse than a standard button looks on Vista
// so we use this custom alpha blending function to  draw disabled icons
// and it matches the standard toolbar implementation pretty well.  The bonus is that our custom
// draw icons now actually look better than standard ones on XP.
//
static void DrawDisabledIcon(HDC dc, CRect rect, HICON hIcon)
{
    WTL::CDC dcMem(CreateCompatibleDC(dc));
    BITMAPINFO bmi;
    ZeroMemory(&bmi, sizeof(BITMAPINFO));
    bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    bmi.bmiHeader.biWidth = rect.Width();
    bmi.bmiHeader.biHeight = rect.Height();
    bmi.bmiHeader.biPlanes = 1;
    bmi.bmiHeader.biBitCount = 32;
    bmi.bmiHeader.biCompression = BI_RGB;
    bmi.bmiHeader.biSizeImage = bmi.bmiHeader.biWidth * bmi.bmiHeader.biHeight * 4;
    VOID *pvBits;

    WTL::CBitmap Bitmap(::CreateDIBSection(dcMem, &bmi, DIB_RGB_COLORS, &pvBits, NULL, 0));
    WTL::CBitmapHandle PrevBitmap(dcMem.SelectBitmap(Bitmap));

    // Fill the background with the transparency color so we can make it transparent later.
    CRect rect2(0, 0, rect.Width(), rect.Height());
    WTL::CBrush brush;
    brush.CreateSolidBrush(RGB(255,0,255));
    FillRect(dcMem, &rect2, brush);

    // Actually draw the icon.
    ::DrawIconEx(dcMem, 0, 0, hIcon, rect.Width(), rect.Height(), 0, 0, DI_NORMAL);  

    // convert to grayscale
    for (unsigned char *p = (unsigned char*)pvBits, *end = p + bmi.bmiHeader.biSizeImage; p < end; p += 4)
    {
        // Make RGB(255,0,255) transparent.
        if( (p[2] == 255) && (p[1] == 0) && (p[0] == 255) )
        {
            p[0] = p[1] = p[2] = p[3] = 0;
        }
        else
        {
            // Gray = 0.3*R + 0.59*G + 0.11*B
            p[0] = p[1] = p[2] = (static_cast<unsigned int>(p[2]) * 77 +
                                  static_cast<unsigned int>(p[1]) * 150 +
                                  static_cast<unsigned int>(p[0]) * 28) >> 8;
            // And the Alpha channel
            p[3] = 255;
        }
    }
   
    BLENDFUNCTION BlendFunction;
    BlendFunction.BlendOp = AC_SRC_OVER;
    BlendFunction.BlendFlags = 0;
    // Respect the full transparency in the source.
    BlendFunction.AlphaFormat = AC_SRC_ALPHA;
    // Make everything else about 50% transparent
    BlendFunction.SourceConstantAlpha = 0x7F;
   
    // use bitmap alpha
    AlphaBlend(dc, rect.left, rect.top,
               bmi.bmiHeader.biWidth,bmi.bmiHeader.biHeight,
               dcMem, 0, 0,
               bmi.bmiHeader.biWidth, bmi.bmiHeader.biHeight,
               BlendFunction);
   
    dcMem.SelectBitmap(PrevBitmap);
}