显示图标的简单菜单 - 极简方法
一种超级简便的自绘菜单实现方法。
引言
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 Frisch、b ga、DarkWeaver5455和Joe Partridge的代码评审。请注意b ga的评论,其中展示了如何自定义绘制图标以反映高亮或禁用状态。
2007年2月25日的更新解决了Joe Partridge指出的资源泄露问题。演示项目Zip文件已更新,以反映文章中发布的代码。它也可以在VC6中编译(这些更改未在文章正文中体现)。
最新更新(2008年1月21日)展示了如何在Windows 2000上显示32位图标。