MFC 实现托盘图标菜单图标功能
💡 MFC 实现托盘图标菜单图标功能
在开发 Windows 应用程序时,我们经常会使用托盘(系统通知区域)图标作为程序的入口,并在其上弹出右键菜单。很多初学者在尝试为托盘菜单项添加图标时,会陷入一个误区:为什么我用了 AppendMenu(MF_STRING, ...)
加了文字,却无法加上图标?明明图标也加载了,却看不到。
答案是:你需要使用 MF_OWNERDRAW
才能实现托盘菜单带图标的效果。
🎯 一、AppendMenu 和 InsertMenu 中 lpNewItem 的陷阱
在 MFC 或 Win32 API 中,我们常使用如下函数添加菜单项:
BOOL AppendMenu(HMENU hMenu,UINT uFlags,UINT_PTR uIDNewItem,LPCTSTR lpNewItem
);
其中最后一个参数 lpNewItem
表示“菜单项的内容”,但这个参数含义随 uFlags
改变而改变,这是很多人第一次没搞懂的地方。
🧠 各种 uFlags
对 lpNewItem
的影响如下:
uFlags 包含标志 | lpNewItem 含义 |
---|---|
MF_STRING (默认) | 指向一个字符串,用作菜单显示文本 |
MF_BITMAP | 位图句柄(HBITMAP),用于菜单图标 |
MF_OWNERDRAW | 任意值,由开发者在自绘时解释(一般传 ID) |
MF_SEPARATOR | 忽略 lpNewItem ,用作分隔线 |
MF_POPUP | 忽略 lpNewItem ,uIDNewItem 为子菜单句柄 |
🧩 二、MF_STRING 的局限性:无法显示图标
当你这样写时:
menu.AppendMenu(MF_STRING, ID_TRAY_EXIT, _T("退出程序"));
- ✅ 系统会自动显示一行带“退出程序”的菜单项;
- ❌ 但你无法设置图标,即使用
SetMenuItemInfo
设置MIIM_BITMAP
也无效; - ❌ 也不会调用你的
OnDrawItem
或OnMeasureItem
,因为它不是“自绘”菜单项。
结论是:MF_STRING
菜单项由系统自动绘制,你无法干预它的样式。
✅ 三、为什么使用 MF_OWNERDRAW 可以实现图标菜单?
当你这样写:
menu.AppendMenu(MF_OWNERDRAW, ID_TRAY_EXIT, (LPCTSTR)ID_TRAY_EXIT);
此时告诉系统:这个菜单项由我自己绘制!
随后你会收到:
WM_MEASUREITEM
消息 → 你告诉系统菜单项的高度与宽度;WM_DRAWITEM
消息 → 你用 GDI 画背景、图标、文字;
你可以在 OnDrawItem
中使用:
DrawIconEx(pDC->GetSafeHdc(), x, y, hIcon, 16, 16, 0, NULL, DI_NORMAL);
从而绘制你想要的图标、选中背景、分隔线、文字样式等内容。
✨ 四、HBMMENU_CALLBACK 配合 MF_OWNERDRAW 使用的关键
除了 MF_OWNERDRAW
,我们还需要用 SetMenuItemInfo
设置菜单图标绘制方式为:
mii.fMask = MIIM_BITMAP;
mii.hbmpItem = HBMMENU_CALLBACK;
menu.SetMenuItemInfo(ID_TRAY_EXIT, &mii);
这告诉系统:“菜单图标我自己来画(callback)”。这一步对实现图标显示非常关键。
⚠️ 注意:HBMMENU_CALLBACK 只有在 MF_OWNERDRAW 下才会被调用!
📌 五、完整对比示例:有图 VS 无图
🚫 传统写法(只能显示文字):
menu.AppendMenu(MF_STRING, ID_TRAY_EXIT, _T("退出程序"));
✅ 支持图标的写法:
menu.AppendMenu(MF_OWNERDRAW, ID_TRAY_EXIT, (LPCTSTR)ID_TRAY_EXIT);// 设置图标绘制方式
MENUITEMINFO mii = { sizeof(MENUITEMINFO) };
mii.fMask = MIIM_BITMAP;
mii.hbmpItem = HBMMENU_CALLBACK;
menu.SetMenuItemInfo(ID_TRAY_EXIT, &mii);// 响应 WM_MEASUREITEM 与 WM_DRAWITEM
🧪 六、你必须响应的两个函数
void OnMeasureItem(...) // 设置菜单项大小(宽度、高度)
void OnDrawItem(...) // 绘制图标 + 文字 + 背景等
在 OnDrawItem
中可以自由绘制图标,例如:
DrawIconEx(hdc, rc.left + 4, rc.top + 4, hIconExit, 16, 16, 0, NULL, DI_NORMAL);
再配合 DrawText
绘制文字,形成如下效果:
📌 🛑 退出程序
📌 🔄 恢复窗口
📃 七、完整代码
// SGMeasurementDlg.h: 头文件
//#pragma once// CSGMeasurementDlg 对话框
class CSGMeasurementDlg : public CDialogEx
{
// 构造
public:CSGMeasurementDlg(CWnd* pParent = nullptr); // 标准构造函数// 对话框数据
#ifdef AFX_DESIGN_TIMEenum { IDD = IDD_SGMEASUREMENT_DIALOG };
#endifprotected:virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持// 实现
protected:HICON m_hIcon;// 生成的消息映射函数virtual BOOL OnInitDialog();afx_msg void OnSysCommand(UINT nID, LPARAM lParam);afx_msg void OnPaint();afx_msg HCURSOR OnQueryDragIcon();afx_msg void OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct);afx_msg void OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct);afx_msg void OnClose();afx_msg LRESULT OnTrayIconClick(WPARAM wParam, LPARAM lParam);afx_msg void OnTrayRestore();afx_msg void OnTrayExit();DECLARE_MESSAGE_MAP()private:// === 托盘图标管理 ===/*** @brief 托盘图标相关数据结构(NOTIFYICONDATA)*/NOTIFYICONDATA m_trayIconData;/*** @brief 托盘图标的唯一 ID*/UINT m_nTrayIconID;/*** @brief 标记托盘图标是否已成功创建*/BOOL m_bTrayIconCreated;/*** @brief 标记程序是否通过托盘图标退出*/BOOL m_bExitingFromTray;
};
// SGMeasurementDlg.cpp: 实现文件
//#include "pch.h"
#include "framework.h"
#include "SGMeasurement.h"
#include "SGMeasurementDlg.h"
#include "afxdialogex.h"#ifdef _DEBUG
#define new DEBUG_NEW
#endif// 托盘图标 ID 与消息宏
#define ID_TRAY_RESTORE 2001 // 恢复窗口
#define ID_TRAY_EXIT 2002 // 退出程序
#define WM_TRAY_ICON_NOTIFY (WM_USER + 1000) // 托盘图标回调消息 ID// 托盘提示文本宏
#define TRAY_ICON_TOOLTIP_TEXT _T("SGMeasurement")// 计时宏定义
#define MEASURE_FUNC_START() \clock_t __startClock = clock();#define MEASURE_FUNC_END() \do { \clock_t __endClock = clock(); \double __elapsedMs = 1000.0 * (__endClock - __startClock) / CLOCKS_PER_SEC; \CString __strElapsed; \__strElapsed.Format(_T("%s 执行耗时:%.1f ms"), _T(__FUNCTION__), __elapsedMs); \AppendLogLineRichStyled(__strElapsed, LOG_COLOR_SUCCESS); \} while (0)class CAboutDlg : public CDialogEx
{
public:CAboutDlg();// 对话框数据
#ifdef AFX_DESIGN_TIMEenum { IDD = IDD_ABOUTBOX };
#endifprotected:virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持// 实现
protected:DECLARE_MESSAGE_MAP()
};CAboutDlg::CAboutDlg() : CDialogEx(IDD_ABOUTBOX)
{
}void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{CDialogEx::DoDataExchange(pDX);
}BEGIN_MESSAGE_MAP(CAboutDlg, CDialogEx)
END_MESSAGE_MAP()CSGMeasurementDlg::CSGMeasurementDlg(CWnd* pParent /*=nullptr*/): CDialogEx(IDD_SGMEASUREMENT_DIALOG, pParent), m_nTrayIconID(0), m_bTrayIconCreated(FALSE), m_bExitingFromTray(FALSE)
{m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}void CSGMeasurementDlg::DoDataExchange(CDataExchange* pDX)
{CDialogEx::DoDataExchange(pDX);
}BEGIN_MESSAGE_MAP(CSGMeasurementDlg, CDialogEx)ON_WM_SYSCOMMAND()ON_WM_PAINT()ON_WM_QUERYDRAGICON()ON_WM_MEASUREITEM()ON_WM_DRAWITEM()ON_WM_CLOSE()ON_MESSAGE(WM_TRAY_ICON_NOTIFY, &CSGMeasurementDlg::OnTrayIconClick)ON_COMMAND(ID_TRAY_RESTORE, &CSGMeasurementDlg::OnTrayRestore)ON_COMMAND(ID_TRAY_EXIT, &CSGMeasurementDlg::OnTrayExit)
END_MESSAGE_MAP()BOOL CSGMeasurementDlg::OnInitDialog()
{CDialogEx::OnInitDialog();// 将“关于...”菜单项添加到系统菜单中。// IDM_ABOUTBOX 必须在系统命令范围内。ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);ASSERT(IDM_ABOUTBOX < 0xF000);CMenu* pSysMenu = GetSystemMenu(FALSE);if (pSysMenu != nullptr){BOOL bNameValid;CString strAboutMenu;bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);ASSERT(bNameValid);if (!strAboutMenu.IsEmpty()){pSysMenu->AppendMenu(MF_SEPARATOR);pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);}}// 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动// 执行此操作SetIcon(m_hIcon, TRUE); // 设置大图标SetIcon(m_hIcon, FALSE); // 设置小图标// TODO: 在此添加额外的初始化代码// 托盘图标初始化m_trayIconData.cbSize = sizeof(NOTIFYICONDATA); // 设置托盘图标数据结构的大小m_trayIconData.hWnd = m_hWnd; // 设置窗口句柄m_trayIconData.uID = m_nTrayIconID; // 设置托盘图标 IDm_trayIconData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; // 设置托盘图标的标志(图标、消息、提示文本)m_trayIconData.uCallbackMessage = WM_TRAY_ICON_NOTIFY; // 设置回调消息 WM_TRAY_ICON_NOTIFYm_trayIconData.hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME); // 加载托盘图标lstrcpy(m_trayIconData.szTip, TRAY_ICON_TOOLTIP_TEXT); // 设置托盘提示文本// 添加托盘图标Shell_NotifyIcon(NIM_ADD, &m_trayIconData);m_bTrayIconCreated = TRUE;return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}void CSGMeasurementDlg::OnSysCommand(UINT nID, LPARAM lParam)
{if ((nID & 0xFFF0) == IDM_ABOUTBOX){CAboutDlg dlgAbout;dlgAbout.DoModal();}else{CDialogEx::OnSysCommand(nID, lParam);}
}void CSGMeasurementDlg::OnPaint()
{if (IsIconic()) {CPaintDC dc(this);SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);// 使图标在工作区矩形中居中int cxIcon = GetSystemMetrics(SM_CXICON);int cyIcon = GetSystemMetrics(SM_CYICON);CRect rect;GetClientRect(&rect);int x = (rect.Width() - cxIcon + 1) / 2;int y = (rect.Height() - cyIcon + 1) / 2;// 绘制图标dc.DrawIcon(x, y, m_hIcon);}else {CDialogEx::OnPaint();}
}//当用户拖动最小化窗口时系统调用此函数取得光标显示。
HCURSOR CSGMeasurementDlg::OnQueryDragIcon()
{return static_cast<HCURSOR>(m_hIcon);
}void CSGMeasurementDlg::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{if (lpMeasureItemStruct->CtlType == ODT_MENU) {lpMeasureItemStruct->itemHeight = 24;lpMeasureItemStruct->itemWidth = 140;}
}void CSGMeasurementDlg::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct)
{if (lpDrawItemStruct->CtlType != ODT_MENU) { return;}CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);CRect rc = lpDrawItemStruct->rcItem;UINT id = lpDrawItemStruct->itemID;// 背景COLORREF bgColor = (lpDrawItemStruct->itemState & ODS_SELECTED) ? RGB(200, 220, 255) : RGB(255, 255, 255);pDC->FillSolidRect(rc, bgColor);// 图标HICON hIcon = nullptr;if (id == ID_TRAY_RESTORE) {hIcon = AfxGetApp()->LoadIcon(IDI_ICON_RESTORE);}if (id == ID_TRAY_EXIT) {hIcon = AfxGetApp()->LoadIcon(IDI_ICON_EXIT);}if (hIcon) {DrawIconEx(pDC->GetSafeHdc(), rc.left + 4, rc.top + 4, hIcon, 16, 16, 0, NULL, DI_NORMAL);}// 文本CString str;if (id == ID_TRAY_RESTORE) { str = _T("恢复界面");}if (id == ID_TRAY_EXIT) { str = _T("退出程序");}pDC->SetBkMode(TRANSPARENT);pDC->SetTextColor(RGB(0, 0, 0));pDC->DrawText(str, CRect(rc.left + 28, rc.top, rc.right, rc.bottom), DT_SINGLELINE | DT_VCENTER | DT_LEFT);
}void CSGMeasurementDlg::OnClose()
{// TODO: 在此添加消息处理程序代码和/或调用默认值if (m_bExitingFromTray) {// 从托盘退出流程ExitApplication();}else {// 正常关闭按钮int nResult = AfxMessageBox(_T("是否最小化到托盘?"), MB_YESNO | MB_ICONQUESTION);if (nResult == IDYES) {ShowWindow(SW_HIDE);}else {ExitApplication();}}
}LRESULT CSGMeasurementDlg::OnTrayIconClick(WPARAM wParam, LPARAM lParam) {if (wParam == m_nTrayIconID) {if (LOWORD(lParam) == WM_LBUTTONUP) {// 左键点击恢复窗口ShowWindow(SW_SHOW);SetForegroundWindow();}else if (LOWORD(lParam) == WM_RBUTTONUP) {// 右键点击弹出菜单CMenu menu;menu.CreatePopupMenu();menu.AppendMenu(MF_OWNERDRAW, ID_TRAY_RESTORE, (LPCTSTR)ID_TRAY_RESTORE);menu.AppendMenu(MF_OWNERDRAW, ID_TRAY_EXIT, (LPCTSTR)ID_TRAY_EXIT);// 加载图标HICON hIconRestore = (HICON)::LoadImage(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_ICON_RESTORE), IMAGE_ICON, 16, 16, LR_SHARED);HICON hIconExit = (HICON)::LoadImage(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_ICON_EXIT), IMAGE_ICON, 16, 16, LR_SHARED);// 设置图标到菜单项MENUITEMINFO mii = { sizeof(MENUITEMINFO) };mii.fMask = MIIM_BITMAP;// 恢复菜单项图标mii.hbmpItem = HBMMENU_CALLBACK;menu.SetMenuItemInfo(ID_TRAY_RESTORE, &mii);// 退出菜单项图标mii.hbmpItem = HBMMENU_CALLBACK;menu.SetMenuItemInfo(ID_TRAY_EXIT, &mii);// 获取鼠标当前位置,并显示菜单POINT pt;GetCursorPos(&pt);SetForegroundWindow();menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, this);}}return 0;
}void CSGMeasurementDlg::OnTrayRestore()
{ShowWindow(SW_SHOW); // 恢复窗口SetForegroundWindow(); // 将窗口置于前端
}void CSGMeasurementDlg::OnTrayExit()
{// 从托盘图标菜单选择“退出程序”if (AfxMessageBox(_T("确定要退出程序吗?"), MB_YESNO | MB_ICONQUESTION) == IDYES) {m_bExitingFromTray = TRUE;PostMessage(WM_CLOSE);}
}
✅ 八、总结与推荐
使用方式 | 是否支持图标 | 适合场景 |
---|---|---|
MF_STRING | ❌ | 普通菜单项 |
MF_BITMAP | ⚠️(位图,失真严重) | 已过时,不推荐 |
MF_OWNERDRAW + HBMMENU_CALLBACK | ✅ 支持完整图标绘制 | 推荐!托盘菜单、图标菜单项 |
✅ 推荐实践:
- 想让托盘菜单显示图标:请务必使用
MF_OWNERDRAW
; - 搭配
WM_MEASUREITEM
/WM_DRAWITEM
精准绘制菜单外观; lpNewItem
的内容只是标识 ID,可强转(LPCTSTR)ID_MENU
;- 如果不做自绘,系统不会调用你的菜单绘制函数!
🏁 参考阅读与补充
- MSDN 官方文档 - AppendMenu function