65.9K
CodeProject 正在变化。 阅读更多。
Home

显示图标的简单菜单 - 极简方法

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.37/5 (15投票s)

2006年11月26日

CPOL

4分钟阅读

viewsIcon

192400

downloadIcon

3416

一种超级简便的自绘菜单实现方法。

Sample Image - ICON_menus.png

引言

CodeProject上许多关于菜单图片的文章都需要深入的理解、大量的自定义代码,并且会生成一个完全依赖于新类的应用程序。我们的尝试是生成一个简单的复制粘贴结构,用户输入通过自然的方式完成,即Microsoft Visual Studio内置的资源编辑器。

该代码是用Visual Studio 2005和Visual C++ 6构建的,并在Windows XP和Windows 2000上进行了测试。

构建你的MFC应用程序所需

首先,确保你的应用程序能够正常工作。然后,将以下三个函数添加到你的CMainFrame类中(如果你的应用程序是基于对话框的,则添加到对话框类中)。

    afx_msg void OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpdis);
    afx_msg void OnInitMenuPopup(CMenu* pMenu, UINT nIndex, BOOL bSysMenu);
    afx_msg void OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpmis);
           HMENU GetIconForItem(UINT itemID) const;

添加这些函数的消息映射条目

    ON_WM_DRAWITEM()
    ON_WM_MEASUREITEM()
    ON_WM_INITMENUPOPUP()

放在BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)END_MESSAGE_MAP()之间。

最后,将这四个函数粘贴到你的CPP文件中。

HICON CMainFrame::GetIconForItem(UINT itemID) const
{
    HICON hIcon = (HICON)0;

    if (IS_INTRESOURCE(itemID))
    {    
        hIcon = (HICON)::LoadImage(::AfxGetResourceHandle(), 
                MAKEINTRESOURCE(itemID), IMAGE_ICON, 0, 0, 
                LR_DEFAULTCOLOR | LR_SHARED);
    }

    if (!hIcon)
    {
        CString sItem; // look for a named item in resources

        GetMenu()->GetMenuString(itemID, sItem, MF_BYCOMMAND);
        sItem.Replace(_T(' '), _T('_'));
        // cannot have resource items with space in name

        if (!sItem.IsEmpty())
            hIcon = (HICON)::LoadImage(::AfxGetResourceHandle(), sItem, 
                     IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR | LR_SHARED);
    }
    return hIcon;
}

void CMainFrame::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpdis)
{
    if ((lpdis==NULL)||(lpdis->CtlType != ODT_MENU))
    {
        CFrameWnd::OnDrawItem(nIDCtl, lpdis);
        return; //not for a menu
    }

    HICON hIcon = GetIconForItem(lpdis->itemID);
    if (hIcon)
    {
        ICONINFO iconinfo;
        ::GetIconInfo(hIcon, &iconinfo);

        BITMAP bitmap;
        ::GetObject(iconinfo.hbmColor, sizeof(bitmap), &bitmap);
        ::DeleteObject(iconinfo.hbmColor);
        ::DeleteObject(iconinfo.hbmMask);

        ::DrawIconEx(lpdis->hDC, lpdis->rcItem.left, lpdis->rcItem.top, 
                     hIcon, bitmap.bmWidth, bitmap.bmHeight, 0, NULL, DI_NORMAL);
//      ::DestroyIcon(hIcon); // we use LR_SHARED instead
    }
}

void CMainFrame::OnInitMenuPopup(CMenu* pMenu, UINT nIndex, BOOL bSysMenu)
{
    AfxTrace(_T(__FUNCTION__) _T(": %#0x\n"), pMenu->GetSafeHmenu());
    CFrameWnd::OnInitMenuPopup(pMenu, nIndex, bSysMenu);

    if (bSysMenu)
    {
        pMenu = GetSystemMenu(FALSE);
    }
    MENUINFO mnfo;
    mnfo.cbSize = sizeof(mnfo);
    mnfo.fMask = MIM_STYLE;
    mnfo.dwStyle = MNS_CHECKORBMP | MNS_AUTODISMISS;
    pMenu->SetMenuInfo(&mnfo);

    MENUITEMINFO minfo;
    minfo.cbSize = sizeof(minfo);

    for (UINT pos=0; pos < pMenu->GetMenuItemCount(); pos++)
    {
        minfo.fMask = MIIM_FTYPE | MIIM_ID;
        pMenu->GetMenuItemInfo(pos, &minfo, TRUE);

        HICON hIcon = GetIconForItem(minfo.wID);

        if (hIcon && !(minfo.fType & MFT_OWNERDRAW))
        {
            AfxTrace(_T("replace for \"%s\" id=%u width=%d\n"), 
                    (LPCTSTR)sItem, (WORD)minfo.wID, 0); // size.cx);

            minfo.fMask = MIIM_FTYPE | MIIM_BITMAP;
            minfo.hbmpItem = HBMMENU_CALLBACK;
            minfo.fType = MFT_STRING;

            pMenu->SetMenuItemInfo(pos, &minfo, TRUE);
        }
        else
            AfxTrace(_T("keep for %s id=%u\n"), (LPCTSTR)sItem, (WORD)minfo.wID);
//        ::DestroyIcon(hIcon); // we use LR_SHARED instead
    }
}

void CMainFrame::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpmis)
{
    if ((lpmis==NULL)||(lpmis->CtlType != ODT_MENU))
    {
        CFrameWnd::OnMeasureItem(nIDCtl, lpmis); //not for a menu
        return;
    }

    lpmis->itemWidth = 16;
    lpmis->itemHeight = 16;

    CString sItem;
    GetMenu()->GetMenuString(lpmis->itemID, sItem, MF_BYCOMMAND);

    HICON hIcon = GetIconForItem(lpmis->itemID);

    if (hIcon)
    {
        ICONINFO iconinfo;
        ::GetIconInfo(hIcon, &iconinfo);

        BITMAP bitmap;
        ::GetObject(iconinfo.hbmColor, sizeof(bitmap), &bitmap);
        ::DeleteObject(iconinfo.hbmColor);
        ::DeleteObject(iconinfo.hbmMask);

        lpmis->itemWidth = bitmap.bmWidth;
        lpmis->itemHeight = bitmap.bmHeight;

        AfxTrace(_T(__FUNCTION__) _T(": %d \"%s\"%dx%d ==> %dx%d\n"), 
                (WORD)lpmis->itemID, (LPCTSTR)sItem, bitmap.bmWidth, 
                bitmap.bmHeight, lpmis->itemWidth, lpmis->itemHeight);
    }
}

现在,你可以编译你的应用程序,你会发现没有任何变化。要添加图标到某些菜单项旁边,就像截图中的那样,你只需要在资源中添加图标。图标的ID应该与菜单的ID相同。就是这样。

图标本身负责漂亮地渲染自己。图标大小无关紧要。一个好的图标编辑器(我使用带有图标插件的Paint.NET可以创建任意大小和颜色的图标。

有时,这还不够。不幸的是,包含子菜单的菜单项没有菜单ID。或者至少,你无法用资源编辑器设置这样的ID。对于这些情况,你可以添加一个名称与子菜单名称对应的图标。像这样:

ICONS                   ICON                    "res\\lock.ico"
...
IDR_MAINFRAME MENU 
BEGIN
    POPUP "&File"
    BEGIN
        POPUP "Icons"

将菜单文本映射到图标的方法使用下划线(_)来替换空格字符;另外,请注意,你可以在图标标识符中使用&字符,但有一个技巧:Windows资源管理器会将这样的图标识别为列表中的第一个,并用它来表示你的可执行文件。解决方法:为用于调用IDR_MAINFRAME的图标设置标识符&(一个字符)。

揭示的一些魔法

我们扫描菜单以待显示,并添加一个标志,表示该项的位图应该由所有者绘制。如果资源文件为该项提供了图标,则从图标中提取位图。WM_MEASUREITEM消息仅请求位图的大小。

请注意,灰色、默认等所有样式仍然可用。不幸的是,当项目被高亮(选中)时,灰色图标会以全彩色显示。你需要一个特殊函数(在b ga的评论中发布)来覆盖此行为。

出于美学原因,我们将菜单样式设置为MNS_CHECKORBMP。但是,如果这些带有图标的某些项目被选中,则复选标记将覆盖自定义绘制回调。另一方面,所呈现的方法可以很容易地推广以显示自定义彩色复选标记。

关于菜单栏的一些话

这里介绍的技术确实适用于菜单栏(即始终显示在窗口客户区上方部分,如文件编辑视图帮助),但效果并不完美(例如,下划线会覆盖图像),并且需要修改Windows提供给OnDrawItem函数的矩形。无论如何,所附代码(在压缩的演示中)也会在菜单栏中绘制图标。

如果你有32位(XP风格)图标和Win2K怎么办?

听起来可能有点好笑,但直到最近,我才遇到一个在Windows 2000上显示真彩色32位图标的要求。在Windows XP上,你只需要调用DrawIconEx(hdc, left, top, hIcon, width, height, 0, NULL, DI_NORMAL);。然而,在Windows 2K上,这个API会忽略alpha通道。以下是与旧版Windows兼容的片段。请注意,32位图标是表示带有alpha通道的位图的简便方法。图标格式不受预定义方形尺寸的限制,并且实际上比32位BMP格式支持得更好。我个人使用Paint.NET的ICO插件来生成此类资源。

static inline unsigned int alphaBlend(const unsigned int bg, const unsigned int src)
{
    unsigned int    a = src >> 24;    // sourceColor alpha

    // If source pixel is transparent, just return the background
    if (0 == a) return bg;
    if (255 == a) return src;

    // alpha-blend the src and bg colors
    unsigned int rb = (((src & 0x00ff00ff) * a) + 
          ((bg & 0x00ff00ff) * (0xff - a))) & 0xff00ff00;
    unsigned int    g  = (((src & 0x0000ff00) * a) + 
          ((bg & 0x0000ff00) * (0xff - a))) & 0x00ff0000;

    return (src & 0xff000000) | ((rb | g) >> 8);
}

void MyDrawIcon(HDC hdc, int iconID, int left=0, int top=0, int width=0, int height=0)
{
    if (iconID <= 0)
        return;

    HICON hIcon = LoadIcon(iconID);

    if (!hIcon)
    {
#ifdef _DEBUG
        static bool once = true;
        if (once)
        {
            once = false;
            char str[100];
            HWND hwnd = WindowFromDC(hdc);
            if (GetDlgCtrlID(hwnd))
            {
                sprintf_s(str, "iconID=%d is unknown for control=%d", 
                          iconID, GetDlgCtrlID(hwnd));
                MessageBoxA(GetParent(hwnd), str, "Debug", MB_OK | MB_APPLMODAL);
            }
            else
            {
                sprintf_s(str, "iconID=%d is unknown for window=%#x", iconID, hwnd);
                MessageBoxA(hwnd, str, "Debug", MB_OK | MB_APPLMODAL);
            }
        }
#endif
        return;
    }

#if 1 // WIN2K
    ICONINFO iconInfo;
    GetIconInfo(hIcon, &iconInfo);
    if (iconInfo.hbmMask)
    {
        BITMAP bm;
        GetObject(iconInfo.hbmMask, sizeof(bm), &bm);
        DeleteBitmap(iconInfo.hbmMask);
    }

    if (!iconInfo.hbmColor)
    {
#ifdef _DEBUG
        static bool once = true;
        if (once)
        {
            once = false;
            char str[100];
            HWND hwnd = WindowFromDC(hdc);
            if (GetDlgCtrlID(hwnd))
            {
                sprintf_s(str, "iconInfo.hbmColor is NULL for control=%d", 
                          GetDlgCtrlID(hwnd));
                MessageBoxA(GetParent(hwnd), str, "Debug", MB_OK | MB_APPLMODAL);
            }
            else
            {
                sprintf_s(str, "iconInfo.hbmColorhbmColor is NULL for window=%#x", hwnd);
                MessageBoxA(hwnd, str, "Debug", MB_OK | MB_APPLMODAL);
            }
        }
#endif
        return;
    }

    BITMAP bm;
    GetObject(iconInfo.hbmColor, sizeof(bm), &bm);

    if (width == 0)
        width = bm.bmWidth;

    if (height == 0)
        height = bm.bmHeight;

    if (bm.bmBitsPixel != 32)
    {
#ifdef _DEBUG
        static bool once = true;
        if (once)
        {
            once = false;
            char str[100];
            HWND hwnd = WindowFromDC(hdc);
            if (GetDlgCtrlID(hwnd))
            {
                sprintf_s(str, "iconInfo.hbmColor Bits/Pixel=%d" + 
                          " is not correct for control=%d", 
                          bm.bmBitsPixel, GetDlgCtrlID(hwnd));
                MessageBoxA(GetParent(hwnd), str, "Debug", MB_OK | MB_APPLMODAL);
            }
            else
            {
                sprintf_s(str, "iconInfo.hbmColor Bits/Pixel=%d" + 
                          " is not correct for window=%#x", bm.bmBitsPixel, hwnd);
                MessageBoxA(hwnd, str, "Debug", MB_OK | MB_APPLMODAL);
            }
        }
        DeleteBitmap(iconInfo.hbmColor);
#endif
        return;
    }

    BITMAPINFO bmi = { sizeof(BITMAPINFOHEADER) };
    // get bitmap info
    GetDIBits(hdc, iconInfo.hbmColor, 0, bm.bmHeight, NULL, &bmi, DIB_RGB_COLORS);
    // prepare pixel buffer; note we use 32 bits per pixel
    LPDWORD iconBits = (LPDWORD)malloc(bmi.bmiHeader.biSizeImage);
    // get pixels
    GetDIBits(hdc, iconInfo.hbmColor, 0, bm.bmHeight, iconBits, &bmi, DIB_RGB_COLORS);

    // if width and height are specified, use these for destination bitmap
    bmi.bmiHeader.biWidth = width;
    bmi.bmiHeader.biHeight = height;

    HDC hdcMem = CreateCompatibleDC(hdc);
    LPDWORD pBitsDest = NULL;
    HBITMAP hBmpDest = CreateDIBSection(hdcMem, &bmi, DIB_RGB_COLORS, 
                                       (void **)&pBitsDest, NULL, 0);
    HBITMAP hOld = SelectBitmap(hdcMem, hBmpDest);

    // copy the background to memory DC; the pBitsDest buffer will reflect the change
    HWND hwnd = WindowFromDC(hdc);
    if (IsWindow(hwnd) && GetDlgCtrlID(hwnd)) // this is a dialog child
    {
        RECT rc;
        GetWindowRect(hwnd, &rc);
        ScreenToClient(GetParent(hwnd), (LPPOINT)&rc);
        HDC parentDC = GetDC(GetParent(hwnd));
        BitBlt(hdcMem, 0, 0, width, height, parentDC, rc.left+left, rc.top+top, SRCCOPY);
        ReleaseDC(GetParent(hwnd), parentDC);
    }
    else
    {
        BitBlt(hdcMem, 0, 0, width, height, hdc, left, top, SRCCOPY);
    }

    // tile the alpha mask image if the size does not fit
    for (int y=0, ys=0; y < height; y++, (++ys < bm.bmHeight) || (ys = 0))
    {
        for (int x=0, xs=0; x < width; x++, (++xs < bm.bmWidth) || (xs = 0))
        {
            *pBitsDest = alphaBlend(*pBitsDest, iconBits[xs + ys*bm.bmWidth]);
            pBitsDest++;
        }
    }

    // the bitmap has changed, select it and draw it
    SelectBitmap(hdcMem, hBmpDest);
    BitBlt(hdc, left, top, width, height, hdcMem, 0, 0, SRCCOPY);

    SelectBitmap(hdcMem, hOld);
    DeleteDC(hdcMem);
    DeleteBitmap(iconInfo.hbmColor);
    DeleteBitmap(hBmpDest);
    free(iconBits);

#else
    DrawIconEx(hdc, left, top, hIcon, width, height, 0, NULL, DI_NORMAL);
#endif
}

致谢和更新

感谢所有评论者,特别是Gernot Frischb gaDarkWeaver5455Joe Partridge的代码评审。请注意b ga的评论,其中展示了如何自定义绘制图标以反映高亮或禁用状态。

2007年2月25日的更新解决了Joe Partridge指出的资源泄露问题。演示项目Zip文件已更新,以反映文章中发布的代码。它也可以在VC6中编译(这些更改未在文章正文中体现)。

最新更新(2008年1月21日)展示了如何在Windows 2000上显示32位图标。

© . All rights reserved.