第一类 控件消息
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_HSCROLL是 MFC 用来处理"水平滚动条消息" 的消息映射宏
语法形式为:
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:
-
鼠标移动到窗口上方
-
鼠标移动到窗口某个子控件上
-
窗口需要决定应该显示什么鼠标指针
所以 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_KILLFOCUS是 MFC 里处理"窗口失去焦点"消息的宏 ,对应的是 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_SETFOCUS是 MFC 中处理"获得焦点"消息的宏 ,对应 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_CHAR是 MFC 中处理键盘字符输入的消息映射宏 ,对应 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)
当用户在指定控件(如
CListCtrl、CTreeCtrl等)上双击时,就会调用指定的成员函数
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) ,这是一个非常常见且重要的通知消息,尤其用于自定义外观的控件(例如
CListCtrl、CTreeCtrl、CButton、CHeaderCtrl等)。
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_NOTIFYITEMDRAW 或 CDRF_DODEFAULT |
CDDS_ITEMPREPAINT |
绘制单个项之前 | 可以改变颜色、字体等,返回 CDRF_NEWFONT 或 CDRF_DODEFAULT |
CDDS_ITEMPOSTPAINT |
绘制项之后 | 用于额外绘制,如边框、图标 |
CDDS_POSTPAINT |
控件全部绘制完 | 一般很少用 |
NM_CUSTOMDRAW 通常由以下控件发送:
| 控件类型 | 说明 |
|---|---|
CListCtrl |
列表项自定义绘制 |
CTreeCtrl |
节点自定义绘制 |
CButton / CProgressCtrl |
按钮或进度条自定义外观 |
CHeaderCtrl |
表头自定义绘制 |
CTabCtrl |
标签页样式自定义 |
- 改变颜色、字体、背景、边框、绘制自定义内容
ON_NOTIFY(LVN_ITEMCHANGED, id, memberFxn)
表示 列表控件(
CListCtrl)中某一项的状态发生了变化,这是CListCtrl最常用的通知之一,用于响应用户选择、勾选、焦点移动等操作
- 检测选中项变化
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);
}
- 判断复选框状态变化(如果列表启用了复选框)
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_EDITCHANGE 和 ON_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_CHANGE是 MFC(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_REFLECT是 MFC 里用于处理WM_NOTIFY类通知消息的"反射机制"宏。
很多"高级控件"(不是简单按钮)发送的不是 WM_COMMAND,而是:
👉 WM_NOTIFY
比如:
CListCtrlCTreeCtrlCTabCtrl
它们发的是各种 NM_xxx / LVN_xxx / TVN_xxx 通知
其他和ON_CONTROL_REFLECT类似