MFC常见消息映射(简洁版)

第一类 控件消息

ON_COMMAND

当用户点击菜单、工具栏按钮、或某些控件(如按钮控件)时,会触发 WM_COMMAND 消息

cpp 复制代码
ON_COMMAND(command_id, memberFxn)
  • command_id:命令的 ID(通常来自菜单、工具栏或控件的资源 ID)
  • memberFxn:对应的响应函数(通常是类的成员函数)

ON_COMMAND_RANGE

让多个连续命令 ID(如菜单、工具栏按钮)共用同一个消息处理函数。这样可以避免写一堆重复的ON_COMMAND(ID_XXX, func)

cpp 复制代码
ON_COMMAND_RANGE(idFirst, idLast, memberFxn)
  • idFirst:连续命令 ID 的起始值
  • idLast:连续命令 ID 的结束值
  • memberFxn:处理函数名,必须带参数: void memberFxn(UINT nID)

ON_UPDATE_COMMAND_UI

菜单/按钮/快捷键的UI更新,比如:灰显/切换/选中

cpp 复制代码
ON_UPDATE_COMMAND_UI(ID_MENU_ITEM, &CYourClass::OnUpdateMenuItem)

对应的函数定义为:

cpp 复制代码
void CYourClass::OnUpdateMenuItem(CCmdUI* pCmdUI)
{
// 通过 pCmdUI 操作界面元素
}

pCmdUI 提供了若干操作函数:

  • Enable() 控可用
  • SetCheck() 控勾选
  • SetText() 改文字
  • SetRadio() 改单选
  • 自动触发不用管

ON_UPDATE_COMMAND_UI_RANGE

一次性处理一系列连续 ID 的菜单项或工具栏按钮的 UI 更新。

cpp 复制代码
ON_UPDATE_COMMAND_UI_RANGE(ID_START, ID_END, &CYourClass::OnUpdateCommandRange)

对应的函数原型:

cpp 复制代码
void CMainFrame::OnUpdateCommandRange(CCmdUI* pCmdUI)
{
    // 判断当前模式是否与菜单ID对应
    // m_nID 定位谁
    BOOL bChecked = (pCmdUI->m_nID == m_nCurrentMode);
    pCmdUI->SetCheck(bChecked ? 1 : 0);
}

ON_BN_CLICKED

按钮被单击(Click)时触发回调函数。

cpp 复制代码
ON_BN_CLICKED(按钮ID, &CYourClass::OnButtonClicked)

对应的函数定义:

cpp 复制代码
void CYourClass::OnButtonClicked();
  • 注意: 函数必须是 afx_msg void 类型,且没有参数。

第二类 窗口消息

ON_WM_SIZE()

窗口变大或变小时,MFC 就会自动调用 OnSize() 函数。

cpp 复制代码
ON_WM_SIZE()

对应的成员函数定义为:

cpp 复制代码
afx_msg void OnSize(UINT nType, int cx, int cy);

参数说明:

参数 含义
nType 窗口大小变化的类型(比如最小化、最大化、普通调整)
cx 新窗口的宽度(client area)
cy 新窗口的高度(client area)

常见 nType 取值

常量 含义
SIZE_RESTORED 普通拖动调整大小
SIZE_MINIMIZED 最小化
SIZE_MAXIMIZED 最大化
SIZE_MAXSHOW / SIZE_MAXHIDE 窗口显示或隐藏时

ON_WM_TIMER()

在 MFC 中,OnTimer() 就是定时器回调函数

消息映射宏:

cpp 复制代码
ON_WM_TIMER()

对应的成员函数:

cpp 复制代码
afx_msg void OnTimer(UINT_PTR nIDEvent);

参数说明:

参数 含义
nIDEvent 定时器的 ID,用来区分多个不同的定时器

启动定时器

cpp 复制代码
SetTimer(1, 1000, NULL);

关闭定时器

cpp 复制代码
KillTimer(1);

ON_WM_CLOSE()

用户请求关闭时

消息映射:

cpp 复制代码
ON_WM_CLOSE()

对应函数原型:

cpp 复制代码
afx_msg void OnClose();
  • 注意:必须调用基类的 OnClose(),否则窗口不会真正关闭。

与 OnDestroy 的区别

特点 OnClose() OnDestroy()
触发时机 用户请求关闭时 窗口即将销毁时
可阻止关闭 ✅ 可以(不调用基类) ❌ 不可阻止
典型用途 弹确认框、保存配置、取消关闭 释放资源、KillTimer、DeleteObject
是否要调用基类 ✅ 通常需要 ✅ 通常需要
  • **ON_WM_CLOSE**是"用户想关闭窗口时"的拦截点,可以提示、保存、清理、阻止关闭。

ON_WM_DESTROY()

窗口即将被销毁(但还没完全释放)时

在类的消息映射表中写:

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyWnd, CWnd)
ON_WM_DESTROY()
END_MESSAGE_MAP()

然后实现对应的函数:

cpp 复制代码
void CMyWnd::OnDestroy()
{
    CWnd::OnDestroy();   // ⚠️ 一定要调用基类的 OnDestroy()

    // 在这里做清理工作
    KillTimer(1);           // 例如:销毁定时器
    delete m_pMyObject;     // 释放动态内存
    m_pMyObject = nullptr;

    TRACE("窗口被销毁了\\n");
}

ON_WM_CREATE()

窗口被创建时

在消息映射表中写:

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyWnd, CWnd)
ON_WM_CREATE()
END_MESSAGE_MAP()

对应实现函数:

cpp 复制代码
int CMyWnd::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CWnd::OnCreate(lpCreateStruct) == -1)
        return -1;  // ⚠️ 必须保留这行,创建失败要返回 -1

    // 在这里初始化
    m_pEdit = new CEdit();
    m_pEdit->Create(WS_CHILD | WS_VISIBLE | WS_BORDER,
                    CRect(10, 10, 200, 30), this, 1001);

    TRACE("窗口创建完成,执行OnCreate\\n");
    return 0;  // 成功
}

WM_CREATE 的发送顺序大致如下:

阶段 调用时机
PreCreateWindow() 在创建窗口前调用,可修改样式
OnCreate()(WM_CREATE) 窗口句柄已创建,可创建子控件
OnShowWindow() 窗口显示时
OnPaint() 开始绘制内容
  • OnCreate() 是窗口生命周期中最早能安全创建子控件的时机。
  • 在 MFC 中,构造函数做逻辑初始化,OnCreate 做UI创建和资源加载。

ON_WM_PAINT()

窗口 需要重绘

在消息映射表中添加:

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyWnd, CWnd)
	ON_WM_PAINT()
END_MESSAGE_MAP()

然后实现:

cpp 复制代码
void CMyWnd::OnPaint()
{
    CPaintDC dc(this); // 必须存在的对象,用于绘制区域的准备和清理

    // 在这里进行绘制
    dc.TextOut(10, 10, _T("Hello, MFC!"));
    dc.Rectangle(50, 50, 150, 100);
}

WM_PAINT 触发时机

触发场景 说明
窗口第一次显示时 系统要求绘制初始内容
窗口被遮挡后重新显示 系统要求重绘被遮挡部分
调用 Invalidate() / RedrawWindow() 手动触发重绘
调整窗口大小 部分区域需要更新

双缓冲绘制防闪烁

cpp 复制代码
void CMyWnd::OnPaint()
{
    CPaintDC dc(this);
    CRect rect;
    GetClientRect(&rect);

    CDC memDC;
    memDC.CreateCompatibleDC(&dc);
    CBitmap bmp;
    bmp.CreateCompatibleBitmap(&dc, rect.Width(), rect.Height());
    CBitmap* pOld = memDC.SelectObject(&bmp);

    memDC.FillSolidRect(rect, RGB(255,255,255));
    memDC.TextOut(20, 20, _T("双缓冲绘制示例"));

    dc.BitBlt(0, 0, rect.Width(), rect.Height(), &memDC, 0, 0, SRCCOPY);

    memDC.SelectObject(pOld);
}

与其他绘图相关消息区别

触发时机 用途
ON_WM_PAINT 系统要求窗口重绘时 主绘图函数
ON_WM_ERASEBKGND 绘制背景前 用于自定义背景或减少闪烁
ON_WM_DRAWITEM 绘制自定义控件项 Owner-Draw 控件使用
ON_WM_PRINTCLIENT 打印客户区内容 适用于控件复制显示

ON_WM_QUERYDRAGICON()

当用户 拖动一个最小化的窗口(在任务栏或桌面上)

在消息映射表中写:

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyDlg, CDialogEx)
    ON_WM_QUERYDRAGICON()
END_MESSAGE_MAP()

然后实现对应函数:

cpp 复制代码
HCURSOR CMyDlg::OnQueryDragIcon()
{
    // 返回用于拖动窗口时的图标句柄
    return static_cast<HCURSOR>(m_hIcon);
}

这个消息最常见于 对话框(Dialog-based)MFC 程序

cpp 复制代码
BOOL CMyDlg::OnInitDialog()
{
    CDialogEx::OnInitDialog();

    // 设置图标(大图标和小图标)
    SetIcon(m_hIcon, TRUE);   // 大图标
    SetIcon(m_hIcon, FALSE);  // 小图标

    return TRUE;
}

HCURSOR CMyDlg::OnQueryDragIcon()
{
    return static_cast<HCURSOR>(m_hIcon);
}
  • 这样,当对话框最小化时,你拖动窗口(例如从任务栏或标题栏图标)时,系统会使用 m_hIcon 显示拖动光标

消息触发时机:

事件 说明
窗口被最小化后 系统准备绘制最小化窗口图标时
用户拖动最小化窗口 系统请求一个光标图像
系统绘制任务栏/桌面窗口 需要图标显示时
  • OnQueryDragIcon() 是在 窗口最小化时被拖动 或 被系统显示为图标时 调用的。

ON_WM_MOUSEMOVE()

它是 鼠标移动消息 WM_MOUSEMOVE 的消息映射宏

当鼠标在窗口客户区内移动 时,MFC 会向窗口发送 WM_MOUSEMOVE,你用 ON_WM_MOUSEMOVE 让你的类接收这个消息。

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyWnd, CWnd)
    ON_WM_MOUSEMOVE()
END_MESSAGE_MAP()
cpp 复制代码
void CMyWnd::OnMouseMove(UINT nFlags, CPoint point)
{
    if (m_rcButton.PtInRect(point))
    {
        if (!m_bHot)
        {
            m_bHot = TRUE;
            Invalidate();
        }
    }
    else
    {
        if (m_bHot)
        {
            m_bHot = FALSE;
            Invalidate();
        }
    }

    CWnd::OnMouseMove(nFlags, point);
}

ON_WM_MOUSEWHEEL()

ON_WM_MOUSEWHEEL鼠标滚轮消息 的消息映射宏,用来响应鼠标滚轮滚动事件。

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyWnd, CWnd)
    ON_WM_MOUSEWHEEL()
END_MESSAGE_MAP()
cpp 复制代码
BOOL CMyWnd::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{
    if (zDelta > 0)
    {
        // 向上滚:放大
        m_scale += 0.1f;
    }
    else
    {
        // 向下滚:缩小
        m_scale -= 0.1f;
    }

    Invalidate(FALSE);
    return TRUE; // 已处理
}

ON_WM_RBUTTONUP()

ON_WM_RBUTTONUP 是 MFC 中的一个消息映射,用于处理 鼠标右键松开WM_RBUTTONUP)事件。

cpp 复制代码
BEGIN_MESSAGE_MAP(CYourWndClass, CWnd)
    ...
    ON_WM_RBUTTONUP()
END_MESSAGE_MAP()
cpp 复制代码
void CYourWndClass::OnRButtonUp(UINT nFlags, CPoint point)
{
    ClientToScreen(&point);

    CMenu menu;
    menu.LoadMenu(IDR_MY_MENU);
    CMenu* pSub = menu.GetSubMenu(0);

    pSub->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
}
  • 常用与:弹出右键菜单,但不推荐

ON_WM_CONTEXTMENU()

ON_WM_CONTEXTMENU 是 MFC 的 右键菜单消息映射宏

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyWnd, CWnd)
    ON_WM_CONTEXTMENU()
END_MESSAGE_MAP()
cpp 复制代码
void CMyWnd::OnContextMenu(CWnd* pWnd, CPoint point)
{
    CMenu menu;
    menu.LoadMenu(IDR_MENU_RIGHT);

    CMenu* pSub = menu.GetSubMenu(0);
    if (pSub)
    {
        pSub->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON,
                             point.x, point.y, this);
    }
}
  • 典型用法(显示自己的右键菜单)

ON_WM_HSCROLL

ON_WM_HSCROLLMFC 用来处理"水平滚动条消息" 的消息映射宏

语法形式为:

cpp 复制代码
afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);

详细说一下:

含义
SB_LINELEFT 点左箭头
SB_LINERIGHT 点右箭头
SB_PAGELEFT 点左空白
SB_PAGERIGHT 点右空白
SB_THUMBTRACK 正在拖动滑块
SB_THUMBPOSITION 拖动结束
SB_LEFT 直接到最左
SB_RIGHT 直接到最右

简单实例:

cpp 复制代码
void CMyWnd::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pBar)
{
    if (nSBCode == SB_THUMBTRACK)
    {
        // 根据 nPos 更新内容偏移
        m_offsetX = nPos;
        Invalidate();   // 重绘
    }

    CWnd::OnHScroll(nSBCode, nPos, pBar);
}

✅ 窗口满足以下之一:

  • 有 **WS_HSCROLL**样式

  • 有 **CScrollBar**子控件

  • 子窗口滚动,消息冒泡到父窗口

ON_WM_VSCROLL

ON_WM_VSCROLL = 处理"垂直滚动条"的消息

对应函数原型

cpp 复制代码
afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
  • 其他内容同上:ON_WM_HSCROLL

ON_WM_ERASEBKGND

Windows 在重绘控件或窗口前,会先"擦背景"(涂成某种颜色),避免出现旧内容。这时就会发 WM_ERASEBKGND 给窗口

最常用的写法 (减少闪烁)

cpp 复制代码
BOOL CYourWnd::OnEraseBkgnd(CDC* pDC)
{
    return TRUE;   // 什么都不做,不让系统擦背景
}

ON_WM_SETCURSOR

Windows 在下面这两种情况下会发送 WM_SETCURSOR:

  1. 鼠标移动到窗口上方

  2. 鼠标移动到窗口某个子控件上

  3. 窗口需要决定应该显示什么鼠标指针

所以 WM_SETCURSOR = "让我决定鼠标指针应该是什么形状"。

cpp 复制代码
​ON_WM_SETCURSOR()

你需要实现这个函数:

cpp 复制代码
BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);

参数含义:

参数 含义
pWnd 鼠标在上面的窗口(可能是自己,也可能是子控件)
nHitTest 鼠标在哪?(客户区、边框、标题栏...)
message 输入消息类型,一般不用

示例:把鼠标变成小手(常用于可点击的自绘控件)

cpp 复制代码
BOOL CMyWnd::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
    if (nHitTest == HTCLIENT)
    {
        ::SetCursor(AfxGetApp()->LoadStandardCursor(IDC_HAND));
        return TRUE;    // 我控制光标,不让系统管
    }

    return CWnd::OnSetCursor(pWnd, nHitTest, message);
}

ON_WM_INITMENUPOPUP

右键菜单 / 菜单栏的一个子菜单 正准备弹出时,系统会把这个消息发给窗口,让你有机会:

映射:

cpp 复制代码
ON_WM_INITMENUPOPUP()

需要实现:

cpp 复制代码
afx_msg void OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu);
参数 意义
pPopupMenu 正要弹出的那个子菜单对象
nIndex 子菜单在上一级菜单的索引
bSysMenu 是否为系统菜单(右键标题栏的)

实例:

cpp 复制代码
void CMyWnd::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu)
{
    if (!pPopupMenu)
        return;

    // 1. 根据功能状态启用/禁用
    pPopupMenu->EnableMenuItem(ID_EDIT_COPY, 
        m_canCopy ? MF_ENABLED : MF_GRAYED);

    pPopupMenu->EnableMenuItem(ID_EDIT_PASTE, 
        m_canPaste ? MF_ENABLED : MF_GRAYED);

    // 2. 设置勾选
    pPopupMenu->CheckMenuItem(ID_VIEW_SHOWLOG,
        m_showLog ? MF_CHECKED : MF_UNCHECKED);

    // 3. 修改文本
    pPopupMenu->ModifyMenu(ID_FILE_SAVE, MF_BYCOMMAND | MF_STRING,
        ID_FILE_SAVE, m_isModified ? _T("保存 *") : _T("保存"));

    CWnd::OnInitMenuPopup(pPopupMenu, nIndex, bSysMenu);
}

ON_WM_KILLFOCUS

ON_WM_KILLFOCUSMFC 里处理"窗口失去焦点"消息的宏 ,对应的是 Windows 消息 WM_KILLFOCUS

消息映射

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyEdit, CEdit)
    ON_WM_KILLFOCUS()
END_MESSAGE_MAP()

重写函数

cpp 复制代码
void CMyEdit::OnKillFocus(CWnd* pNewWnd)
{
    CEdit::OnKillFocus(pNewWnd);

    // pNewWnd:新的焦点窗口
    TRACE(_T("我失去焦点了,新焦点是:%p\n"), pNewWnd);
}

和 ON_EN_KILLFOCUS 的区别

作用对象 本质
ON_WM_KILLFOCUS 所有窗口(CWnd级别) Windows消息
ON_EN_KILLFOCUS Edit控件专用 控件通知

ON_WM_SETFOCUS

ON_WM_SETFOCUSMFC 中处理"获得焦点"消息的宏 ,对应 Windows 消息 WM_SETFOCUS

当一个窗口或控件获得输入焦点时触发,比如:

  • 鼠标点击到某个输入框 ✅
  • 用 Tab 键切换到某个控件 ✅
  • 程序调用 SetFocus()

消息映射

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyEdit, CEdit)
    ON_WM_SETFOCUS()
END_MESSAGE_MAP()

处理函数

cpp 复制代码
void CMyEdit::OnSetFocus(CWnd* pOldWnd)
{
    CEdit::OnSetFocus(pOldWnd);

    // pOldWnd:之前拥有焦点的窗口
    TRACE(_T("我获得焦点了,之前是:%p\n"), pOldWnd);
}

ON_WM_CHAR

ON_WM_CHARMFC 中处理键盘字符输入的消息映射宏 ,对应 Windows 消息 WM_CHAR

当用户在控件(常见是 Edit、自定义控件)中输入字符时触发,例如:

  • 输入 'a''1'
  • 按回车(Enter)✅
  • 按退格(Backspace)✅

消息映射

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyEdit, CEdit)
    ON_WM_CHAR()
END_MESSAGE_MAP()

处理函数

cpp 复制代码
void CMyEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
    CEdit::OnChar(nChar, nRepCnt, nFlags);

    TRACE(_T("输入字符: %c\n"), nChar);
}

ON_WM_SETTINGCHANGE

这个消息在系统设置发生变化时发送

在消息映射里写:

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyWnd, CWnd)
    ON_WM_SETTINGCHANGE()
END_MESSAGE_MAP()

然后实现函数:

cpp 复制代码
afx_msg void OnSettingChange(UINT uFlags, LPCTSTR lpszSection);

ON_WM_SYSCOMMAND

当用户或系统触发"窗口级操作"时,会发送这个消息比如:

  • 点击窗口右上角 ❌(关闭)
  • 点击最小化 / 最大化
  • 双击标题栏最大化
  • Alt + 空格 打开系统菜单
  • 选择"移动 / 大小 / 关闭"等菜单项

消息映射

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyWnd, CWnd)
    ON_WM_SYSCOMMAND()
END_MESSAGE_MAP()

响应函数

cpp 复制代码
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);

ON_WM_NCCALCSIZE

WM_NCCALCSIZE ------ 计算窗口"非客户区"大小的消息

区域 内容
客户区 你自己绘制的内容(按钮、列表等)
非客户区(NC) 标题栏、边框、滚动条、系统按钮

消息映射

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyWnd, CWnd)
    ON_WM_NCCALCSIZE()
END_MESSAGE_MAP()

最常见用法:去掉边框 / 标题栏(无边框窗口)

cpp 复制代码
void CMyWnd::OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS* lpncsp)
{
    if (bCalcValidRects)
    {
        // 直接返回,不调用基类
        return;
    }

    CWnd::OnNcCalcSize(bCalcValidRects, lpncsp);
}

第三类 ON_MESSAGE自定义消息

用于处理自定义消息(自定义WM消息)

ON_MESSAGE 用于响应自定义消息(WM_USER 及以上的消息)。

语法形式为:

cpp 复制代码
ON_MESSAGE(消息ID, 函数名)

然后实现对应函数:

cpp 复制代码
LRESULT CMyWnd::OnMyMessage(WPARAM wParam, LPARAM lParam)
{
		// 在这里处理消息
		TRACE("收到自定义消息: %d %d\\n", wParam, lParam);
		return 0; // 返回值类型必须是 LRESULT
}

参数说明 :

参数 类型 含义
wParam WPARAM 通常用于传递整数或小指针
lParam LPARAM 通常用于传递指针或结构体
返回值 LRESULT 返回处理结果

发送消息的两种方式:

cpp 复制代码
// 异步发送(推荐)
::PostMessage(m_hWnd, WM_MY_MESSAGE, 1, 2);
cpp 复制代码
// 同步发送(立即处理)
::SendMessage(m_hWnd, WM_MY_MESSAGE, 1, 2);
  • 推荐从 WM_USER + 100 开始定
  • 注意 ON_MESSAGE 的函数不会自动调用基类同名函数

第四类 高级控件

ON_NOTIFY(NM_CLICK, id, memberFxn)

示例:处理 CListCtrl 单击事件

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyDlg, CDialogEx)
    ON_NOTIFY(NM_CLICK, IDC_LIST1, &CMyDlg::OnNMClickList1)
END_MESSAGE_MAP()

void CMyDlg::OnNMClickList1(NMHDR *pNMHDR, LRESULT *pResult)
{
    LPNMITEMACTIVATE pNMItem = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);

    int nItem = pNMItem->iItem;       // 被点击的行索引
    int nSubItem = pNMItem->iSubItem; // 被点击的列索引(如果是CListCtrl)

    if (nItem >= 0)
    {
        CString text = m_ListCtrl.GetItemText(nItem, 0);
        AfxMessageBox(_T("你点击了第 ") + CString(std::to_wstring(nItem).c_str()) + _T(" 行,内容:") + text);
    }

    *pResult = 0;
}

ON_NOTIFY(NM_DBLCLK, id, memberFxn)

当用户在指定控件(如 CListCtrlCTreeCtrl等)上双击时,就会调用指定的成员函数

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyDlg, CDialogEx)
    ON_NOTIFY(NM_DBLCLK, IDC_LIST1, &CMyDlg::OnNMDblclkList1)
END_MESSAGE_MAP()

void CMyDlg::OnNMDblclkList1(NMHDR *pNMHDR, LRESULT *pResult)
{
    LPNMITEMACTIVATE pNMItem = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
    int nItem = pNMItem->iItem;       // 双击的行号
    int nSubItem = pNMItem->iSubItem; // 双击的列号(如果是CListCtrl)

    if (nItem >= 0)
    {
        CString str = m_ListCtrl.GetItemText(nItem, 0);
        AfxMessageBox(_T("你双击了第 ") + CString(std::to_wstring(nItem).c_str()) +
                      _T(" 行,内容:") + str);
    }

    *pResult = 0;
}

ON_NOTIFY(MCN_SELCHANGE, id, memberFxn)

表示处理的是 MonthCal(月历控件,类名:CMonthCalCtrl)的"日期选择改变"事件

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyDlg, CDialogEx)
    ON_NOTIFY(MCN_SELCHANGE, IDC_MONTHCALENDAR1, &CMyDlg::OnMcnSelchangeMonthcalendar1)
END_MESSAGE_MAP()

void CMyDlg::OnMcnSelchangeMonthcalendar1(NMHDR *pNMHDR, LRESULT *pResult)
{
    LPNMSELCHANGE pSelChange = reinterpret_cast<LPNMSELCHANGE>(pNMHDR);

    SYSTEMTIME stStart = pSelChange->stSelStart;
    SYSTEMTIME stEnd = pSelChange->stSelEnd;

    CString str;
    str.Format(_T("选择日期:%d-%02d-%02d 到 %d-%02d-%02d"),
               stStart.wYear, stStart.wMonth, stStart.wDay,
               stEnd.wYear, stEnd.wMonth, stEnd.wDay);

    AfxMessageBox(str);

    *pResult = 0;
}

NMSELCHANGE 结构体定义如下:

cpp 复制代码
typedef struct tagNMSELCHANGE
{
    NMHDR nmhdr;        // 通用头信息
    SYSTEMTIME stSelStart;  // 选中开始日期
    SYSTEMTIME stSelEnd;    // 选中结束日期(单选模式下,两个值相同)
} NMSELCHANGE, *LPNMSELCHANGE;

ON_NOTIFY(UDN_DELTAPOSID, id, memberFxn)

表示用户点击了微调控件的上下箭头(要改变数值)时触发

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyDlg, CDialogEx)
    ON_NOTIFY(UDN_DELTAPOS, IDC_SPIN1, &CMyDlg::OnDeltaposSpin1)
END_MESSAGE_MAP()

void CMyDlg::OnDeltaposSpin1(NMHDR *pNMHDR, LRESULT *pResult)
{
    LPNMUPDOWN pNMUpDown = reinterpret_cast<LPNMUPDOWN>(pNMHDR);

    int nCur = pNMUpDown->iPos;     // 当前值
    int nDelta = pNMUpDown->iDelta; // 改变量方向

    int nNewValue = nCur - nDelta;  // 注意:上箭头是 -1,因此要减

    // 限制范围
    if (nNewValue < 0) nNewValue = 0;
    if (nNewValue > 100) nNewValue = 100;

    CString str;
    str.Format(_T("当前值: %d → 新值: %d"), nCur, nNewValue);
    AfxMessageBox(str);

    // 如果希望自己处理更新(不让控件自动修改),可以返回 1
    *pResult = 0;  // 允许控件更新值
}

NMUPDOWN 结构体定义如下:

cpp 复制代码
typedef struct tagNMUPDOWN
{
    NMHDR hdr;      // 通用消息头
    int iPos;       // 当前值
    int iDelta;     // 用户点击的方向(上:负值,下:正值)
} NMUPDOWN, *LPNMUPDOWN;

ON_NOTIFY(BCN_DROPDOWN, id, memberFxn)

表示处理的是 "带下拉箭头的按钮(Split Button)点击下拉部分" 的通知消息。

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyDlg, CDialogEx)
    ON_NOTIFY(BCN_DROPDOWN, IDC_BTN_SPLIT, &CMyDlg::OnBcnDropdownBtnSplit)
END_MESSAGE_MAP()

void CMyDlg::OnBcnDropdownBtnSplit(NMHDR *pNMHDR, LRESULT *pResult)
{
    LPNMBCDROPDOWN pDropDown = reinterpret_cast<LPNMBCDROPDOWN>(pNMHDR);

    // 获取按钮矩形位置
    CRect rc = pDropDown->rcButton;
    CWnd* pWnd = GetDlgItem(IDC_BTN_SPLIT);

    // 转换为屏幕坐标
    pWnd->ClientToScreen(&rc);

    // 创建弹出菜单
    CMenu menu;
    menu.CreatePopupMenu();
    menu.AppendMenu(MF_STRING, ID_MENU_OPTION1, _T("选项 1"));
    menu.AppendMenu(MF_STRING, ID_MENU_OPTION2, _T("选项 2"));
    menu.AppendMenu(MF_STRING, ID_MENU_OPTION3, _T("选项 3"));

    // 在下拉箭头下方显示菜单
    menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, rc.left, rc.bottom, this);

    *pResult = 0;
}

NMBCDROPDOWN结构体定义如下:

cpp 复制代码
typedef struct tagNMBCDROPDOWN {
    NMHDR hdr;      // 通用通知头
    RECT rcButton;  // 按钮的下拉区域矩形
} NMBCDROPDOWN, *LPNMBCDROPDOWN;

ON_NOTIFY(TCN_SELCHANGE, id, memberFxn)

表示这是一个 Tab 控件(选项卡控件,类名:CTabCtrl 的通知消息,用来响应 选项卡切换后(选择已改变) 的事件

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyDlg, CDialogEx)
    ON_NOTIFY(TCN_SELCHANGE, IDC_TAB_MAIN, &CMyDlg::OnTcnSelchangeTabMain)
END_MESSAGE_MAP()

void CMyDlg::OnTcnSelchangeTabMain(NMHDR *pNMHDR, LRESULT *pResult)
{
    int nSel = m_TabCtrl.GetCurSel(); // 获取当前选中页索引

    switch (nSel)
    {
    case 0:
        AfxMessageBox(_T("选中了第 1 个选项卡"));
        break;
    case 1:
        AfxMessageBox(_T("选中了第 2 个选项卡"));
        break;
    case 2:
        AfxMessageBox(_T("选中了第 3 个选项卡"));
        break;
    }

    *pResult = 0;
}

TCN_SELCHANGING 的区别

通知码 触发时机 说明
TCN_SELCHANGING 在切换"之前"发送 通常用于判断是否允许切换(可取消)
TCN_SELCHANGE 在切换"之后"发送 通常用于更新页面显示内容
cpp 复制代码
ON_NOTIFY(TCN_SELCHANGING, IDC_TAB_MAIN, &CMyDlg::OnTcnSelchangingTabMain)
ON_NOTIFY(TCN_SELCHANGE,  IDC_TAB_MAIN, &CMyDlg::OnTcnSelchangeTabMain)

常见用法:切换显示不同的子对话框

cpp 复制代码
void CMyDlg::OnTcnSelchangeTabMain(NMHDR *pNMHDR, LRESULT *pResult)
{
    int nSel = m_TabCtrl.GetCurSel();

    m_Page1.ShowWindow(nSel == 0 ? SW_SHOW : SW_HIDE);
    m_Page2.ShowWindow(nSel == 1 ? SW_SHOW : SW_HIDE);
    m_Page3.ShowWindow(nSel == 2 ? SW_SHOW : SW_HIDE);

    *pResult = 0;
}

ON_NOTIFY(DTN_DATETIMECHANGE, id, memberFxn)

表示这是一个来自 日期时间控件(CDateTimeCtrl 的通知消息,用于处理 用户更改日期或时间 的事件

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyDlg, CDialogEx)
    ON_NOTIFY(DTN_DATETIMECHANGE, IDC_DATETIMEPICKER1, &CMyDlg::OnDtnDatetimechangeDatetimepicker1)
END_MESSAGE_MAP()

void CMyDlg::OnDtnDatetimechangeDatetimepicker1(NMHDR *pNMHDR, LRESULT *pResult)
{
    LPNMDATETIMECHANGE pChange = reinterpret_cast<LPNMDATETIMECHANGE>(pNMHDR);

    if (pChange->dwFlags == GDT_VALID)
    {
        SYSTEMTIME st = pChange->st;

        CString str;
        str.Format(_T("选择的日期时间:%04d-%02d-%02d %02d:%02d:%02d"),
                   st.wYear, st.wMonth, st.wDay,
                   st.wHour, st.wMinute, st.wSecond);

        AfxMessageBox(str);
    }
    else
    {
        AfxMessageBox(_T("当前日期无效(可能用户清空了控件)"));
    }

    *pResult = 0;
}

NMDATETIMECHANGE结构体定义如下:

cpp 复制代码
typedef struct tagNMDATETIMECHANGE
{
    NMHDR nmhdr;         // 通用头信息
    DWORD dwFlags;       // 状态标志,如 GDT_VALID 或 GDT_NONE
    SYSTEMTIME st;       // 当前选择的时间
} NMDATETIMECHANGE, *LPNMDATETIMECHANGE;

主动获取当前时间值:

cpp 复制代码
SYSTEMTIME st;
m_DateTimeCtrl.GetTime(&st);

设置时间:

cpp 复制代码
SYSTEMTIME st = {2025, 10, 27}; // 年月日
m_DateTimeCtrl.SetTime(&st);

ON_NOTIFY(NM_CUSTOMDRAW, id, memberFxn)

表示控件正在请求自定义绘制(Custom Draw) ,这是一个非常常见且重要的通知消息,尤其用于自定义外观的控件(例如 CListCtrlCTreeCtrlCButtonCHeaderCtrl 等)。

cpp 复制代码
void CMyDlg::OnCustomDrawList(NMHDR* pNMHDR, LRESULT* pResult)
{
    LPNMLVCUSTOMDRAW pLVCD = reinterpret_cast<LPNMLVCUSTOMDRAW>(pNMHDR);

    switch (pLVCD->nmcd.dwDrawStage)
    {
    case CDDS_PREPAINT:
        *pResult = CDRF_NOTIFYITEMDRAW;  // 通知要绘制每一项
        return;

    case CDDS_ITEMPREPAINT:
        if (pLVCD->nmcd.dwItemSpec % 2 == 0)
            pLVCD->clrTextBk = RGB(230, 240, 255); // 偶数行淡蓝
        else
            pLVCD->clrTextBk = RGB(255, 255, 255); // 奇数行白色
        *pResult = CDRF_DODEFAULT;
        return;
    }

    *pResult = 0;
}

NMCUSTOMDRAW结构体定义如下:

cpp 复制代码
typedef struct tagNMCUSTOMDRAW {
    NMHDR  hdr;        // 通用消息头
    DWORD  dwDrawStage;// 当前绘制阶段
    HDC    hdc;        // 设备上下文
    RECT   rc;         // 绘制区域
    DWORD  dwItemSpec; // 当前绘制的项(如索引、ID)
    UINT   uItemState; // 项状态(选中、焦点等)
    LPARAM lItemlParam;// 项数据
} NMCUSTOMDRAW, *LPNMCUSTOMDRAW;

绘制阶段(dwDrawStage

常量 含义 返回值(*pResult
CDDS_PREPAINT 开始绘制控件前 返回 CDRF_NOTIFYITEMDRAWCDRF_DODEFAULT
CDDS_ITEMPREPAINT 绘制单个项之前 可以改变颜色、字体等,返回 CDRF_NEWFONTCDRF_DODEFAULT
CDDS_ITEMPOSTPAINT 绘制项之后 用于额外绘制,如边框、图标
CDDS_POSTPAINT 控件全部绘制完 一般很少用

NM_CUSTOMDRAW 通常由以下控件发送:

控件类型 说明
CListCtrl 列表项自定义绘制
CTreeCtrl 节点自定义绘制
CButton / CProgressCtrl 按钮或进度条自定义外观
CHeaderCtrl 表头自定义绘制
CTabCtrl 标签页样式自定义
  • 改变颜色、字体、背景、边框、绘制自定义内容

ON_NOTIFY(LVN_ITEMCHANGED, id, memberFxn)

表示 列表控件(CListCtrl)中某一项的状态发生了变化,这是 CListCtrl 最常用的通知之一,用于响应用户选择、勾选、焦点移动等操作

  1. 检测选中项变化
cpp 复制代码
void CMyDlg::OnListItemChanged(NMHDR* pNMHDR, LRESULT* pResult)
{
    LPNMLISTVIEW pNMListView = (LPNMLISTVIEW)pNMHDR;

    // 判断是否是选中状态改变
    if ((pNMListView->uChanged & LVIF_STATE)
        && (pNMListView->uNewState & LVIS_SELECTED))
    {
        int nIndex = pNMListView->iItem;
        CString strText = m_ListCtrl.GetItemText(nIndex, 0);
        AfxMessageBox(_T("选中了:") + strText);
    }

    *pResult = 0;
}

2.判断焦点变化

cpp 复制代码
if ((pNMListView->uChanged & LVIF_STATE)
    && (pNMListView->uNewState & LVIS_FOCUSED))
{
    TRACE("焦点移动到第 %d 行\\n", pNMListView->iItem);
}
  1. 判断复选框状态变化(如果列表启用了复选框)
cpp 复制代码
if ((pNMListView->uChanged & LVIF_STATE)
    && ((pNMListView->uOldState & LVIS_STATEIMAGEMASK) != (pNMListView->uNewState & LVIS_STATEIMAGEMASK)))
{
    BOOL bChecked = (((pNMListView->uNewState & LVIS_STATEIMAGEMASK) >> 12) - 1);
    TRACE("第 %d 项复选框状态: %s\\n", pNMListView->iItem, bChecked ? "选中" : "未选中");
}

常见变化标志(uChanged

常量 含义
LVIF_STATE 项的状态(选中、焦点、复选框等)发生改变
LVIF_TEXT 文本改变
LVIF_IMAGE 图像索引改变
LVIF_PARAM lParam 改变
LVIF_INDENT 缩进改变
LVIF_COLUMNS 子项列改变
  • 响应选中项、焦点、复选框等变化

ON_CBN_SELCHANGE(id,memberFxn)

当用户在 下拉框(ComboBox)中选择了新的选项 ,就会触发 CBN_SELCHANGE 消息。

cpp 复制代码
BEGIN_MESSAGE_MAP(CYourDlg, CDialogEx)
    ON_CBN_SELCHANGE(IDC_COMBO1, &CYourDlg::OnCbnSelchangeCombo1)
END_MESSAGE_MAP()
cpp 复制代码
void CYourDlg::OnCbnSelchangeCombo1()
{
    int index = m_combo1.GetCurSel(); // 获取当前选中项索引
    CString text;
    m_combo1.GetLBText(index, text);  // 获取文本

    AfxMessageBox(_T("选择变更: ") + text);
}

ON_CBN_EDITCHANGE(id,memberFxn)

ON_CBN_EDITCHANGE组合框(CBS_DROPDOWN / CBS_DROPDOWNLIST) 在用户 修改编辑框内容时 触发的通知消息。

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyDlg, CDialogEx)
    ON_CBN_EDITCHANGE(IDC_MY_COMBO, &CMyDlg::OnCbnEditchangeMyCombo)
END_MESSAGE_MAP()
cpp 复制代码
void CMyDlg::OnCbnEditchangeMyCombo()
{
    CString t;
    m_combo.GetWindowText(t);

    for (int i = t.GetLength()-1; i >= 0; i--)
    {
        if (!_istdigit(t[i]))    // 非数字删除
        {
            t.Delete(i);
        }
    }

    m_combo.SetWindowText(t);
}

ON_CBN_EDITCHANGEON_CBN_SELCHANGE 区别总结

消息 触发时机 示例
ON_CBN_EDITCHANGE 编辑框内容变化 用户输入文字、粘贴、删除
ON_CBN_SELCHANGE 选择改变 用户点击下拉列表中某项

ON_CBN_KILLFOCUS(id,memberFxn)

这是 ComboBox(带编辑框的那种,比如 CBS_DROPDOWN 或 CBS_DROPDOWNLIST)在编辑部分失去焦点时触发的消息通知

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyDlg, CDialogEx)
    ON_CBN_KILLFOCUS(IDC_MY_COMBO, &CMyDlg::OnCbnKillfocusMyCombo)
END_MESSAGE_MAP()
cpp 复制代码
void CMyDlg::OnCbnKillfocusMyCombo()
{
    CString text;
    m_myCombo.GetWindowText(text);

    TRACE(_T("ComboBox 失去焦点,当前文本:%s\n"), text);
}

ON_EN_CHANGE

ON_EN_CHANGEMFC(Microsoft Foundation Classes) 里用来处理编辑框(Edit Control)内容发生变化时的消息映射宏

消息映射里写

cpp 复制代码
BEGIN_MESSAGE_MAP(CYourDlg, CDialogEx)
    ON_EN_CHANGE(IDC_EDIT1, &CYourDlg::OnEnChangeEdit1)
END_MESSAGE_MAP()

对应的处理函数

cpp 复制代码
void CYourDlg::OnEnChangeEdit1()
{
    CString str;
    GetDlgItemText(IDC_EDIT1, str);

    // 这里写你想做的事情
    TRACE(_T("当前输入:%s\n"), str);
}

触发时机

  • 用户手动输入 ✅
  • SetWindowText() 修改内容 ✅(默认会触发!)
  • SetDlgItemText() 修改内容 ✅

第五类 特殊

ON_CONTROL_REFLECT

一、先理解背景(为什么会有它)

在 Windows 原生机制里:

👉 控件(按钮、编辑框等)发生事件时
不会自己处理,而是发消息给父窗口

比如:

  • 按钮点击 → BN_CLICKED
  • 编辑框改变 → EN_CHANGE

👉 默认流程:

控件 → 父窗口(处理)

二、原理

MFC 在中间加了一层"反射"机制:

  • 控件 → 父窗口 → 再发回控件(Reflect)→ 控件自己处理

在控件类内部处理自己的消息,而不是全部写在父窗口里

三、怎么用?

在控件类中写消息映射

cpp 复制代码
BEGIN_MESSAGE_MAP(CMyButton, CButton)
    ON_CONTROL_REFLECT(BN_CLICKED, OnClicked)
END_MESSAGE_MAP()

实现处理函数

cpp 复制代码
void CMyButton::OnClicked()
{
    AfxMessageBox(_T("按钮自己处理点击"));
}

四、常见写法对比(重点)

❌ 传统方式(父窗口处理)

cpp 复制代码
ON_BN_CLICKED(IDC_BUTTON1, OnButtonClick)

👉 写在 CDialog / CWnd

✅ 反射方式(控件自己处理)

cpp 复制代码
ON_CONTROL_REFLECT(BN_CLICKED, OnClicked)

👉 写在 控件类(CButton派生类)里

五、适用场景

✅ 1. 自定义控件(强烈推荐)

比如你写了一个:

  • 自定义按钮
  • 自定义列表项
  • 自定义树节点

👉 希望控件"自带行为",就用这个


✅ 2. 让代码更清晰

  • 控件逻辑写在控件类里
  • 父窗口只负责布局

👉 解耦!

六、注意点(很容易踩坑)

❗1. 必须是"派生控件类"

class CMyButton : public CButton

👉 普通控件(没继承)用不了


❗2. 反射优先级更高 ⭐

👉 如果同时写:

// 控件里

ON_CONTROL_REFLECT(BN_CLICKED, OnClicked)

// 父窗口里

ON_BN_CLICKED(IDC_BUTTON1, OnParentClick)

👉 控件先处理!


❗3. 有些消息有专用宏

比如:

作用
ON_CONTROL_REFLECT 通用
ON_NOTIFY_REFLECT 用于 WM_NOTIFY(更常用)

ON_NOTIFY_REFLECT

ON_NOTIFY_REFLECTMFC 里用于处理 WM_NOTIFY 类通知消息的"反射机制"宏

很多"高级控件"(不是简单按钮)发送的不是 WM_COMMAND,而是:

👉 WM_NOTIFY

比如:

  • CListCtrl
  • CTreeCtrl
  • CTabCtrl

它们发的是各种 NM_xxx / LVN_xxx / TVN_xxx 通知

其他和ON_CONTROL_REFLECT类似

相关推荐
Yupureki2 小时前
《算法竞赛从入门到国奖》算法基础:动态规划-路径dp
数据结构·c++·算法·动态规划
321.。3 小时前
Linux 进程控制深度解析:从创建到替换的完整指南
linux·开发语言·c++·学习
小Tomkk3 小时前
怎么配置 Visual Studio Code 配置 C/C++
c语言·c++·vscode
CheerWWW3 小时前
C++学习笔记——枚举、继承、虚函数、可见性
c++·笔记·学习
比昨天多敲两行3 小时前
C++ AVL树
开发语言·c++
小糯米6013 小时前
C++ 并查集
java·c++·算法
楚Y6同学4 小时前
QT C++ 实现图像查看器
开发语言·c++·qt·图像查看
郝学胜-神的一滴4 小时前
Qt6 + OpenGL 3.3 渲染环境搭建全指南:从空白窗口到专属渲染画布的优雅实现
数据结构·c++·线性代数·算法·系统架构·图形渲染
样例过了就是过了4 小时前
LeetCode热题100 最小栈
数据结构·c++·算法·leetcode