💡 MFC 实现托盘图标菜单图标功能
在开发 Windows 应用程序时,我们经常会使用托盘(系统通知区域)图标作为程序的入口,并在其上弹出右键菜单。很多初学者在尝试为托盘菜单项添加图标 时,会陷入一个误区:为什么我用了 AppendMenu(MF_STRING, ...)
加了文字,却无法加上图标?明明图标也加载了,却看不到。
答案是:你需要使用 MF_OWNERDRAW
才能实现托盘菜单带图标的效果。
🎯 一、AppendMenu 和 InsertMenu 中 lpNewItem 的陷阱
在 MFC 或 Win32 API 中,我们常使用如下函数添加菜单项:
cpp
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 的局限性:无法显示图标
当你这样写时:
cpp
menu.AppendMenu(MF_STRING, ID_TRAY_EXIT, _T("退出程序"));
- ✅ 系统会自动显示一行带"退出程序"的菜单项;
- ❌ 但你无法设置图标 ,即使用
SetMenuItemInfo
设置MIIM_BITMAP
也无效; - ❌ 也不会调用你的
OnDrawItem
或OnMeasureItem
,因为它不是"自绘"菜单项。
结论是:MF_STRING
菜单项由系统自动绘制,你无法干预它的样式。
✅ 三、为什么使用 MF_OWNERDRAW 可以实现图标菜单?
当你这样写:
cpp
menu.AppendMenu(MF_OWNERDRAW, ID_TRAY_EXIT, (LPCTSTR)ID_TRAY_EXIT);
此时告诉系统:这个菜单项由我自己绘制!
随后你会收到:
WM_MEASUREITEM
消息 → 你告诉系统菜单项的高度与宽度;WM_DRAWITEM
消息 → 你用 GDI 画背景、图标、文字;
你可以在 OnDrawItem
中使用:
cpp
DrawIconEx(pDC->GetSafeHdc(), x, y, hIcon, 16, 16, 0, NULL, DI_NORMAL);
从而绘制你想要的图标、选中背景、分隔线、文字样式等内容。
✨ 四、HBMMENU_CALLBACK 配合 MF_OWNERDRAW 使用的关键
除了 MF_OWNERDRAW
,我们还需要用 SetMenuItemInfo
设置菜单图标绘制方式为:
cpp
mii.fMask = MIIM_BITMAP;
mii.hbmpItem = HBMMENU_CALLBACK;
menu.SetMenuItemInfo(ID_TRAY_EXIT, &mii);
这告诉系统:"菜单图标我自己来画(callback)"。这一步对实现图标显示非常关键。
⚠️ 注意:HBMMENU_CALLBACK 只有在 MF_OWNERDRAW 下才会被调用!
📌 五、完整对比示例:有图 VS 无图
🚫 传统写法(只能显示文字):
cpp
menu.AppendMenu(MF_STRING, ID_TRAY_EXIT, _T("退出程序"));
✅ 支持图标的写法:
cpp
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
🧪 六、你必须响应的两个函数
cpp
void OnMeasureItem(...) // 设置菜单项大小(宽度、高度)
void OnDrawItem(...) // 绘制图标 + 文字 + 背景等
在 OnDrawItem
中可以自由绘制图标,例如:
cpp
DrawIconEx(hdc, rc.left + 4, rc.top + 4, hIconExit, 16, 16, 0, NULL, DI_NORMAL);
再配合 DrawText
绘制文字,形成如下效果:
📌 🛑 退出程序
📌 🔄 恢复窗口
📃 七、完整代码
cpp
// SGMeasurementDlg.h: 头文件
//
#pragma once
// CSGMeasurementDlg 对话框
class CSGMeasurementDlg : public CDialogEx
{
// 构造
public:
CSGMeasurementDlg(CWnd* pParent = nullptr); // 标准构造函数
// 对话框数据
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_SGMEASUREMENT_DIALOG };
#endif
protected:
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;
};
cpp
// 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_TIME
enum { IDD = IDD_ABOUTBOX };
#endif
protected:
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; // 设置托盘图标 ID
m_trayIconData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; // 设置托盘图标的标志(图标、消息、提示文本)
m_trayIconData.uCallbackMessage = WM_TRAY_ICON_NOTIFY; // 设置回调消息 WM_TRAY_ICON_NOTIFY
m_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
;- 如果不做自绘,系统不会调用你的菜单绘制函数!