CListCtrl自绘失效原因剖析:从消息反射到NM_CUSTOMDRAW的正确实践
发布时间:2026/10/6 3:39:53 作者:尧图编辑部 阅读量:1,286

先说结论省得你看半天代码还在纳闷CListCtrl 派生类里写 ON_WM_MEASUREITEM_REFLECT 和 ON_WM_DRAWITEM_REFLECT十有八九根本不会被执行。这不是你消息映射写错了也不是 DrawItem 函数签名有问题而是这个消息在 CListCtrl 这条路上压根就没往你的派生类窗口过程里走。这个坑我当年踩了整整一个下午断点打了一排一个都不亮最后把消息机制从头到尾捋了一遍才明白怎么回事。今天就把这个坑掰开揉碎讲清楚顺便把 CListCtrl 自绘的正确姿势给你列全。1. 问题复现反射消息为什么静悄悄1.1 你以为是代码问题其实是消息路径问题先说场景。那时候我要在一个老旧 MFC 项目里做一个资源管理器风格的界面左侧是一棵目录树右侧是一个 CListCtrl要求列表项行高固定 28 像素选中行背景换成自定义的浅蓝色部分列要画进度条和状态图标。这种需求按照我以往自绘 CButton、CListBox 的经验套路非常固定给派生类加两个反射宏重写MeasureItem和DrawItem完事儿。CButton 和 CListBox 的自绘就是这么干的反射消息能直接送到控件自己的窗口过程里派生类里写处理函数天经地义。结果 CListCtrl 让我翻车了。我新建了一个CMyListCtrl : public CListCtrl照葫芦画瓢写了一套// MyListCtrl.h class CMyListCtrl : public CListCtrl { DECLARE_MESSAGE_MAP() public: afx_msg void OnDrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct); afx_msg void OnMeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct); }; // MyListCtrl.cpp BEGIN_MESSAGE_MAP(CMyListCtrl, CListCtrl) ON_WM_DRAWITEM_REFLECT() ON_WM_MEASUREITEM_REFLECT() END_MESSAGE_MAP() void CMyListCtrl::OnDrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { // 断点打在这里永远进不来 CListCtrl::OnDrawItem(lpDrawItemStruct); } void CMyListCtrl::OnMeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct) { // 断点打在这里也永远进不来 lpMeasureItemStruct-itemHeight 28; }然后在初始化时给控件加上了LVS_OWNERDRAWFIXED样式CMyListCtrl* pList (CMyListCtrl*)GetDlgItem(IDC_MY_LIST); DWORD dwStyle GetWindowLong(pList-GetSafeHwnd(), GWL_STYLE); SetWindowLong(pList-GetSafeHwnd(), GWL_STYLE, dwStyle | LVS_OWNERDRAWFIXED);运行起来列表还是老样子一行都没有用我的代码画。断点一个都没亮。1.2 排错第一步先确认是不是姿势不对我当时做了几件排除工作你也可以按这个顺序自查一遍能省不少时间。第一确认消息映射表是否在类声明里有DECLARE_MESSAGE_MAP()。这个一般不会漏如果你是从别的类复制过来的代码容易把DECLARE_MESSAGE_MAP和BEGIN_MESSAGE_MAP这一套落下。少了它整个类都不参与消息路由。第二确认宏名字是否写对。ON_WM_DRAWITEM_REFLECT跟ON_WM_MEASUREITEM_REFLECT中间这段是WM_开头别写成ON_DRAWITEM_REFLECT。MFC 的反射宏统一是ON_ 消息名 _REFLECT编译期如果你的宏写错了编译器会报未定义标识符所以一般不会错到编译都过不了。第三确认样式有没有真的设置上。用GetWindowLong读出来看一眼或者直接用 Spy 查窗口样式。LVS_OWNERDRAWFIXED的值是 0x0040如果在 Spy 里看不到这个样式消息压根就不会产生反射自然也无从谈起。第四确认你是不是把列表放在对话框、FormView、View 还是 Frame 里。这个很关键不同宿主窗口对反射消息的处理能力不一样后面第二部分会详细讲。以上四条如果都排除了那问题就集中在一个点上反射消息REFLECT 消息根本没有被你的宿主窗口转给 CListCtrl 派生类。这就不是代码写错的问题了是消息机制在 CListCtrl 上压根不支持这么玩。2. 消息机制的真相WM_DRAWITEM 到底发给谁2.1 Owner Draw 的原始语义消息发给父窗口先把这个基础概念掰扯清楚。MFC 里的ON_WM_DRAWITEM_REFLECT、ON_WM_MEASUREITEM_REFLECT这类反射宏本质上是微软为了解决一个问题Windows 自绘控件的消息按照 Win32 API 的最初设计是发给 owner window通常是父窗口的不是发给控件自己的。举个最典型的例子一个自绘按钮。你给按钮加上BS_OWNERDRAW样式之后当按钮需要重绘时系统会往按钮的父窗口发送WM_DRAWITEM消息。如果你在按钮自己的窗口过程里等着收WM_DRAWITEM那是什么都等不到的。那为什么我们平时写自绘按钮经常能在按钮派生类里通过DrawItem虚函数处理呢因为 MFC 对几种常见控件做了适配。比如CButton这个类MFC 内部会在按钮被创建后安装一个内部消息处理机制父窗口收到WM_DRAWITEM时会根据消息里的控件 ID 或窗口句柄找到对应的CButton对象然后把消息转给它由它的虚函数DrawItem处理。CListBox、CComboBox 也是类似。这套转发机制就是 MFC 消息反射的雏形。反射宏里的_REFLECT后缀对应的是 MFC 自定义的一组窗口消息WM_REFLECT_BASE WM_USER 0x1C00 ON_WM_DRAWITEM_REFLECT 对应 WM_REFLECT_BASE WM_DRAWITEM ON_WM_MEASUREITEM_REFLECT 对应 WM_REFLECT_BASE WM_MEASUREITEM宿主窗口收到原始的WM_DRAWITEM后如果判断这个控件是 MFC 管理的子窗口就会将它转换成WM_REFLECT_BASE WM_DRAWITEM再发给子控件。子控件的消息映射里有ON_WM_DRAWITEM_REFLECT就能接住。如果宿主窗口不做这层转换子控件里写再多反射宏都是白搭。2.2 CListCtrl 的特殊之处它不是那种“普通”的自绘控件问题来了CListCtrl 到底是哪一类CListCtrl 是通用控件库 comctl32.dll 提供的控件它在系统内部的实现远比按钮、列表框复杂。CListCtrl 支持多种视图模式大图标、小图标、列表、详细信息内部还要处理列头、滚动、编辑框、拖拽排序等一堆逻辑。它的自绘支持其实是后来补上的一种半残功能。MSDN 对LVS_OWNERDRAWFIXED样式的说明原文大意是当 list-view 控件使用这个样式时owner window 必须处理WM_MEASUREITEM和WM_DRAWITEM消息。关键词是 owner window也就是父窗口不是控件本身。这跟你写CButton派生类时的体验完全不是一个路子。更麻烦的是list-view 在 Vista/7 之后也就是 comctl32 v6 时代加载了视觉样式Visual Styles后内部绘制流程完全重写了。你设置LVS_OWNERDRAWFIXED之后老的WM_DRAWITEM消息路径在部分系统版本上行为非常诡异。有的版本会发一次有的版本干脆不发有的版本发了但绘制结果又被系统默认绘制盖掉。这就是为什么很多人说“我父窗口写了OnDrawItem也没用”。2.3 为什么 CButton 行CListCtrl 不行我之前也一直有个疑惑CButton 能反射CListBox 也能反射怎么到 CListCtrl 就不行后来查了 MFC 源码和大量论坛帖子我的理解是MFC 的消息反射分发主要依赖各控件在内部实现了特定的消息处理虚函数而且父窗口类比如 CDialog在HandleReflectMessages或OnWndMsg里做了专门的转发逻辑。CButton、CListBox 这些控件MFC 在底层源码里有意识地针对WM_DRAWITEM、WM_MEASUREITEM做了适配让它们能把自己的子类消息反射回去。而 CListCtrl在 MFC 的设计里被定位为一个 complex control自绘主要靠WM_NOTIFY通知机制走不是靠WM_DRAWITEM。CListCtrl 的派生类里即使写了ON_WM_DRAWITEM_REFLECT但父窗口在消息分发时不会把WM_DRAWITEM转成反射消息发给它最终消息就被当作普通父窗口消息处理掉了你的派生类函数自然永远不会被调用。一句话总结CListCtrl 的自绘消息路径和 CButton/CListBox 不在一条道上。2.4 一个必须知道的补充WM_NOTIFY 反射是真能用的CListCtrl 这套路走不通但有一个反射是真的能让 CListCtrl 派生类稳稳接住的那就是WM_NOTIFY通知的反射。CListCtrl 和父窗口通信主要靠的就是WM_NOTIFY。你往列表里加数据、加列、点击、滚动、绘制过程中的各种通知都是通过WM_NOTIFY发给父窗口的MFC 对此做了完善的反射支持。你在派生类里写ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, OnCustomDraw)这个是绝对能收到的。因为NM_CUSTOMDRAW就是通过WM_NOTIFY发给父窗口的而 MFC 对WM_NOTIFY的反射机制是所有反射里最成熟最可靠的。这一点直接决定了后面你要用的方案不要跟WM_DRAWITEM死磕转去用NM_CUSTOMDRAW。3. 自绘 CListCtrl 的正确路线3.1 方案 A在父窗口中硬接 WM_DRAWITEM / WM_MEASUREITEM既然消息是发给父窗口的那最原始的办法就是在父窗口里直接处理。父窗口类CDialog 或 CFormView重写OnDrawItem和OnMeasureItem在里面判断是不是你那个 CListCtrl 的 ID。// 父窗口 .cpp void CMyDialog::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct) { if (nIDCtl IDC_MY_LIST) { lpMeasureItemStruct-itemHeight 28; return; } CDialogEx::OnMeasureItem(nIDCtl, lpMeasureItemStruct); } void CMyDialog::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct) { if (nIDCtl IDC_MY_LIST) { // 这里能收到可以在这里画 return; } CDialogEx::OnDrawItem(nIDCtl, lpDrawItemStruct); }这个方案优点是思路直接。缺点也明显自绘逻辑全写在父窗口里如果页面里有两三个列表每个列表的画法还不一样这个函数会膨胀成一大坨 if-else可维护性很差。而且WM_MEASUREITEM在 comctl32 v6 下触发的时机很玄学行高经常设置不上去。3.2 方案 B父窗口转发给子控件自己处理既然 CListCtrl 派生类收不到反射消息那我们就在父窗口收到消息后手动把消息转发给控件自己。这样绘制代码可以封装在 CMyListCtrl 类内部比方案 A 干净。父窗口的OnDrawItem里void CMyDialog::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct) { if (nIDCtl IDC_MY_LIST) { CMyListCtrl* pList (CMyListCtrl*)GetDlgItem(IDC_MY_LIST); if (pList ! NULL) { pList-SendMessage(WM_DRAWITEM, IDC_MY_LIST, (LPARAM)lpDrawItemStruct); return; } } CDialogEx::OnDrawItem(nIDCtl, lpDrawItemStruct); }然后 CMyListCtrl 里不要用反射宏改用普通消息映射BEGIN_MESSAGE_MAP(CMyListCtrl, CListCtrl) ON_WM_DRAWITEM() END_MESSAGE_MAP() void CMyListCtrl::OnDrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { // 这里就能进来了 }这个方案能跑通但每次都要在父窗口里手动转发略显繁琐。如果父窗口不是对话框而是自定义的 CWnd你还得保证GetDlgItem能正确查到子控件。真正的问题在后面WM_DRAWITEM自绘 CListCtrl 本身在现代系统上就是个不稳定的路子画出来的效果经常被系统默认绘制覆盖闪烁也严重。所以方案 B 只能算一个过渡性的解法。3.3 方案 CNM_CUSTOMDRAW处理列表自绘的主流方案推荐就用它NM_CUSTOMDRAWCustom Draw是微软给 comctl32 系列控件包括 ListView、TreeView、Toolbar、StatusBar 等设计的自绘通知机制。它不是走WM_DRAWITEM而是走WM_NOTIFY通知。控件的绘制过程被拆成多个阶段pre-paint、item-paint、subitem-paint每个阶段系统发一个NM_CUSTOMDRAW通知你的代码可以决定这一步我全接管还是让系统画或者画完我再叠加。这套机制的好处是消息路径非常明确MFC 对WM_NOTIFY反射支持完善ON_NOTIFY_REFLECT在派生类里就直接能接住绘制阶段化粒度细可以只改行背景、文字颜色不用把所有绘制逻辑都自己撸一遍兼容视觉样式不像WM_DRAWITEM那样在 v6 下失灵不依赖父窗口的额外转发代码封装在控件类内部可复用。我自己现在做 CListCtrl 自绘90% 的情况都是直接上 NM_CUSTOMDRAW。除非你只是要一个固定行高那个用WM_MEASUREITEM在父窗口里简单处理一下也行。4. 实战用 NM_CUSTOMDRAW 实现一个能打的自绘 CListCtrl4.1 准备工作样式和初始化先建一个CMyListCtrl然后在初始化里设置列表样式和扩展样式。这里注意LVS_OWNERDRAWFIXED对于 NM_CUSTOMDRAW 方案来说不是必须的。如果只想固定行高可以加如果主要通过 NM_CUSTOMDRAW 控制所有绘制加不加都行不影响通知。我建议先不加等需要行高时再加避免变量太多。// 初始化代码一般在 OnInitDialog 里 m_list.SetView(LVS_REPORT); m_list.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER);LVS_EX_DOUBLEBUFFER要加不加的话自绘时列表在滚动和局部刷新时会闪烁到你怀疑人生。4.2 在派生类里接住 NM_CUSTOMDRAW头文件里加消息映射和回调// MyListCtrl.h class CMyListCtrl : public CListCtrl { DECLARE_MESSAGE_MAP() public: afx_msg void OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult); };// MyListCtrl.cpp BEGIN_MESSAGE_MAP(CMyListCtrl, CListCtrl) ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, OnCustomDraw) END_MESSAGE_MAP()注意这里用了ON_NOTIFY_REFLECT而不是ON_NOTIFY因为通知发给父窗口后反射机制才把它送回控件自己。如果你在父窗口的OnNotify里看到NM_CUSTOMDRAW没被处理也可以在这里处理但代码就分散了。用ON_NOTIFY_REFLECT直接封在控件类里最合适。4.3 自定义行背景、文字颜色和行高下面写一个最实用的例子自定义行背景色、选中行背景色、文字颜色、固定行高、在状态列画一个简单的色块。void CMyListCtrl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { LPNMLVCUSTOMDRAW pNMLVCD (LPNMLVCUSTOMDRAW)pNMHDR; *pResult CDRF_DODEFAULT; switch (pNMLVCD-nmcd.dwDrawStage) { case CDDS_PREPAINT: // 第一阶段告诉系统后续子项绘制都要通知我 *pResult CDRF_NOTIFYITEMDRAW; break; case CDDS_ITEMPREPAINT: { // 每个 item 绘制之前在这里设置颜色 int nItem (int)pNMLVCD-nmcd.dwItemSpec; if (pNMLVCD-nmcd.uItemState CDIS_SELECTED) { pNMLVCD-clrTextBk RGB(220, 230, 255); // 选中行背景 pNMLVCD-clrText RGB(20, 20, 40); // 选中行文字 } else if (nItem % 2 1) // 斑马纹 { pNMLVCD-clrTextBk RGB(248, 248, 248); pNMLVCD-clrText RGB(50, 50, 50); } else { pNMLVCD-clrTextBk RGB(255, 255, 255); pNMLVCD-clrText RGB(50, 50, 50); } // 如果想自绘文字这里不需要额外处理把颜色返回给系统让它画就行 *pResult CDRF_NOTIFYSUBITEMDRAW; // 允许后续的子项通知用于每列细化 break; } case CDDS_SUBITEM | CDDS_ITEMPREPAINT: { // 每个子项单元格绘制之前可以按列单独控制颜色 int nItem (int)pNMLVCD-nmcd.dwItemSpec; int nSubItem pNMLVCD-iSubItem; if (nSubItem 3) // 假设第 4 列是状态列 { pNMLVCD-clrTextBk RGB(255, 255, 240); *pResult CDRF_NEWFONT; } break; } default: break; } }4.4 如果非要自己画内容怎么画才不翻车用 NM_CUSTOMDRAW 的好处是颜色和背景这类简单需求你设置clrTextBk和clrText剩下的文字绘制、图标绘制、焦点框绘制系统默认就干了完全不用自己写 GDI 代码。但如果你的需求是“整行画一个自定义背景纹理”“某一行要带一个进度条”“状态列要画一个圆形图标”那就需要接管具体的绘制动作。做法是在CDDS_ITEMPREPAINT阶段返回CDRF_SKIPDEFAULT表示“这个 item 的系统默认绘制我不需要了我自己来画”。case CDDS_ITEMPREPAINT: { int nItem (int)pNMLVCD-nmcd.dwItemSpec; CDC* pDC CDC::FromHandle(pNMLVCD-nmcd.hdc); CRect rcItem; GetItemRect(nItem, rcItem, LVIR_BOUNDS); // 画背景 pDC-FillSolidRect(rcItem, RGB(240, 245, 255)); // 画第一列的文字 CString strText GetItemText(nItem, 0); pDC-SetTextColor(RGB(30, 30, 30)); pDC-SetBkMode(TRANSPARENT); CRect rcText rcItem; rcText.left 6; rcText.right rcText.left 200; // 简单起见只画一列区域 pDC-DrawText(strText, rcText, DT_LEFT | DT_VCENTER | DT_SINGLELINE); // 告诉系统这个 item 你已经不用画了 *pResult CDRF_SKIPDEFAULT; break; }这里有几个坑pDC直接用CDC::FromHandle(pNMLVCD-nmcd.hdc)不要CreateCompatibleDC乱来这里传给你的就是列表控件的绘制 DCGetItemRect和GetItemText是CListCtrl的成员函数直接调用没问题画完之后要返回CDRF_SKIPDEFAULT不然系统会把你刚画的内容覆盖掉如果你既要自绘背景又要保留文字绘制不要这么干应该设置颜色让系统画文字你只画背景否则文字部分全得自己处理。4.5 行高到底怎么设置这个放到后面单独说因为很多人卡在这里。CListCtrl设置行高没有直接的SetRowHeight函数。行高受两个因素影响小图标列表ImageList的图标高度以及LVS_OWNERDRAWFIXED样式下WM_MEASUREITEM返回的itemHeight。如果你用 NM_CUSTOMDRAW 自绘且返回了CDRF_SKIPDEFAULT或者需要自适应行高最稳的方式是给控件设置LVS_OWNERDRAWFIXED然后在父窗口的OnMeasureItem里返回指定高度。注意这里真的要在父窗口里写CListCtrl 派生类的ON_WM_MEASUREITEM_REFLECT是接不住的正如你踩的那个坑。// 父窗口里 void CMyDialog::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct) { if (nIDCtl IDC_MY_LIST) { lpMeasureItemStruct-itemHeight 28; return; } CDialogEx::OnMeasureItem(nIDCtl, lpMeasureItemStruct); }另一种更省事的办法用一个小尺寸的 ImageList 作为列表的 state image list直接拉高行高。比如SetImageList一个高度为 28 的 CImageList行高自动变成 28。这个不算正规操作但是在老项目里改动最小如果你不想动父窗口的OnMeasureItem可以试试。4.6 还有几个自绘时容易爆的雷第一个雷子项绘制和整行选中的关系。如果你没开LVS_EX_FULLROWSELECT点击一个子项时默认只有第一列高亮后几列没有选中背景。但 NM_CUSTOMDRAW 处理颜色时如果没开全行选中后几列还是走CDDS_SUBITEM分支你要自己判断选中状态否则会出现半行选中这种非常丑的效果。第二个雷CDDS_ITEMPREPAINT里设置了clrTextBk但实际绘制文字用的背景色不一定是你设置的那个。因为系统绘制文字时用的是SetBkMode和SetTextColor这一套你设置clrTextBk只对系统内部填充背景有效。如果发现颜色不对检查是不是扩展样式里的LVS_EX_GRIDLINES在捣鬼网格线颜色和你设置的背景叠在一起观感会和预期不符。第三个雷自绘时刷新性能。如果你在CDDS_ITEMPREPAINT里做了比较重的 GDI 操作比如加载位图、创建画刷列表一滚动就会卡。优化办法是把画刷、字体、位图都缓存成成员变量只在创建时初始化一次绘制时只选入 DC 不重复创建。能用FillSolidRect就不要CreateSolidBrushFillRect后者开销大不少。5. 常见问题排查清单与个人心得5.1 排查速查表症状可能原因解法派生类OnDrawItem不执行CListCtrl 的WM_DRAWITEM发给父窗口不发给控件本身改用父窗口OnDrawItem或直接上 NM_CUSTOMDRAW派生类OnMeasureItem不执行WM_MEASUREITEM同样发给父窗口MFC 反射未覆盖到 CListCtrl在父窗口处理WM_MEASUREITEM设置行高父窗口OnDrawItem收到但绘制无效comctl32 v6 视觉样式下旧自绘接口支持不好改用NM_CUSTOMDRAWON_NOTIFY_REFLECT(NM_CUSTOMDRAW, ...)不触发父窗口可能拦截并处理了NM_CUSTOMDRAW未走反射在父窗口OnNotify里返回 FALSE或确认没有别的消息映射抢走通知自绘后列表闪烁未开LVS_EX_DOUBLEBUFFER扩展样式加LVS_EX_DOUBLEBUFFER行高设置无效LVS_OWNERDRAWFIXED未设置或WM_MEASUREITEM没有真正触发确认样式、确认在父窗口处理或用 ImageList 高度设置行高5.2 三个最多人踩的误区误区一以为所有控件自绘都一样。实际上按钮、列表框、组合框和列表控件ListView的自绘实现基础完全不一样。前三个可以走 owner-draw 消息反射列表控件在现代系统上就应该直接走 Custom Draw。你把 CButton 那套思路搬到 CListCtrl大概率复制粘贴一时爽排查火葬场。误区二分不清WM_DRAWITEM和NM_CUSTOMDRAW的关系。一个是老的 owner-draw 消息系统一个是通用控件自绘通知机制。它们解决的是不同层面的事情。老代码里可能还混用新代码能用NM_CUSTOMDRAW就不要碰WM_DRAWITEM。误区三把LVS_OWNERDRAWFIXED当成万能钥匙。这个样式只负责告诉系统“owner 要参与绘制”但到底怎么参与、参与哪些部分是WM_DRAWITEM、WM_MEASUREITEM这套老机制说了算。你光加个样式不处理对应的消息等于啥也没干。用 NM_CUSTOMDRAW 时不加这个样式反而更干净。5.3 一个让排错效率翻倍的小习惯遇到自绘不生效先别急着写绘制代码。先在消息处理函数里加一句OutputDebugString(_T(OnCustomDraw called, stage%d\n), pNMLVCD-nmcd.dwDrawStage);然后跑起来看输出。输出里能看到阶段标识你就知道系统目前走到哪一步了哪个阶段没走到就顺着那个阶段往上查消息来源和样式设置。我每次做控件自绘第一步永远是确认消息能不能进来第二步才谈画什么。你这次踩的坑根源就是第一步没确认直接跳到了第二步——宏加了、函数写了以为万事大吉结果消息压根没走到你这层。再分享一个经验如果你要在 CListView 的派生类里做自绘也就是基于 Doc/View 架构的那种处理方式和 CListCtrl 略有区别。CListView 的GetListCtrl()返回的是内部 CListCtrl 的引用自绘通知的处理最好放在CListView派生类里通过ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, ...)映射逻辑是一样的但要注意通知的反射来自 CListCtrl 子控件不是 CListView 本身。这个细节会让不少人卡半天顺手备注一下。