当前位置: 首页 > news >正文

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 改变而改变,这是很多人第一次没搞懂的地方。

🧠 各种 uFlagslpNewItem 的影响如下:

uFlags 包含标志lpNewItem 含义
MF_STRING(默认)指向一个字符串,用作菜单显示文本
MF_BITMAP位图句柄(HBITMAP),用于菜单图标
MF_OWNERDRAW任意值,由开发者在自绘时解释(一般传 ID)
MF_SEPARATOR忽略 lpNewItem,用作分隔线
MF_POPUP忽略 lpNewItemuIDNewItem 为子菜单句柄

🧩 二、MF_STRING 的局限性:无法显示图标

当你这样写时:

menu.AppendMenu(MF_STRING, ID_TRAY_EXIT, _T("退出程序"));
  • ✅ 系统会自动显示一行带“退出程序”的菜单项;
  • ❌ 但你无法设置图标,即使用 SetMenuItemInfo 设置 MIIM_BITMAP 也无效;
  • ❌ 也不会调用你的 OnDrawItemOnMeasureItem,因为它不是“自绘”菜单项。

结论是: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
http://www.lryc.cn/news/607836.html

相关文章:

  • 【相机】曝光时间长-->拖影
  • Effective C++ 条款17:以独立语句将newed对象置入智能指针
  • 易华路副总经理兼交付管理中心部门经理于江平受邀PMO大会主持人
  • Elasticsearch+Logstash+Filebeat+Kibana单机部署
  • RabbitMQ面试精讲 Day 7:消息持久化与过期策略
  • 用Unity结合VCC更改人物模型出现的BUG
  • 个人笔记UDP
  • 内存、硬盘与缓存的技术原理及特性解析
  • C 语言问题
  • 基于结构熵权-云模型的铸铁浴缸生产工艺安全评价
  • filezilla出现connected refused的时候排查问题
  • String boot 接入 azure云TTS
  • Java试题-选择题(4)
  • 防火墙相关技术内容
  • JVM 调优中JVM的参数如何起到调优动作?具体案例,G1GC垃圾收集器参数调整建议
  • JVM学习日记(十四)Day14——性能监控与调优(一)
  • 基于ELK Stack的实时日志分析与智能告警实践指南
  • SpringBoot 信用卡检测、OpenAI gym、OCR结合、DICOM图形处理、知识图谱、农业害虫识别实战
  • JVM 01 运行区域
  • Qwen3 Embedding:新一代文本表征与排序模型
  • Hyper-V + Centos stream 9 搭建K8s集群(一)
  • 手动开发一个TCP客户端调试工具(三):工具界面设计
  • 【人工智能agent】--服务器部署PaddleX 的 印章文本识别模型
  • Design Compiler:Milkyway库的创建与使用
  • 分布式微服务--Nacos作为配置中心(补)关于bosststrap.yml与@RefreshScope
  • 集成电路学习:什么是CMSIS微控制器软件接口标准
  • [创业之路-528]:技术成熟度曲线如何指导创业与投资?
  • UNet改进(28):KD Attention增强UNet的知识蒸馏方法详解
  • 深入解析 <component :is> 在 Vue3 组合式中的使用与局限
  • 【推荐100个unity插件】快速实现汽车控制器——PROMETEO: Car Controller插件