
做MFC控件美化的时候最容易被网上老代码带偏的坑就是自绘CListCtrl时照搬CListBox那套Owner Draw流程。最近我就在CListCtrl派生类里写了ON_WM_MEASUREITEM_REFLECT也重写了DrawItem(LPDRAWITEMSTRUCT lpMeasureItemStruct)样式加上了LVS_OWNERDRAWFIXED可是断点怎么打都不进消息就是没反应。网上搜了一圈同款问题不少答案却五花八门有人说要检查样式有人说要处理反射顺序还有人直接劝你弃用Owner Draw转投NM_CUSTOMDRAW。我把整条链路从样式到消息再到反射彻底排查了一遍才搞清楚CListCtrl的自绘机制和CListBox根本是两码事。这篇文章就把我的排查思路、原理理解、以及最终可复现的自绘方案全部写出来给还在这条路上折腾的人一个明确方向。1. 问题现场自绘CListCtrl反射和DrawItem全没反应1.1 最初按CListBox思路写出的代码我最初的处理方式完完全全是按照自绘CListBox的套路来的。先在CListCtrl的派生类头文件里声明两个重写函数// CMyListCtrl.h class CMyListCtrl : public CListCtrl { public: virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct); virtual void MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct); };然后在源文件里挂上消息映射// CMyListCtrl.cpp BEGIN_MESSAGE_MAP(CMyListCtrl, CListCtrl) ON_WM_MEASUREITEM_REFLECT() ON_WM_DRAWITEM_REFLECT() END_MESSAGE_MAP() void CMyListCtrl::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { // 在这里绘制每一行 } void CMyListCtrl::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct) { // 在这里设置行高 }再在对话框初始化或者PreSubclassWindow里加上Owner Draw样式void CMyListCtrl::PreSubclassWindow() { CListCtrl::PreSubclassWindow(); SetWindowLong(GetSafeHwnd(), GWL_STYLE, GetWindowLong(GetSafeHwnd(), GWL_STYLE) | LVS_OWNERDRAWFIXED); }代码写完编译没问题运行也没报错——但列表长得和之前一模一样DrawItem和MeasureItem里的断点从头到尾没触发过。这个现象很典型不是语法错误也不是调用错误而是整个自绘机制根本就没走这条路。1.2 三个典型症状我总结下当时遇到的三个表现如果你也全中那基本可以确定是同一个原因断点不进入DrawItem和MeasureItem无论怎么刷新、滚动、点击行一个断点都不触发。控件行为完全正常列表能显示、能选中、能滚动说明不是控件损坏也不是消息被异常吃掉。单独把LVS_OWNERDRAWFIXED加上后又去掉列表外观没有任何变化说明这个样式对CListCtrl的实际绘制路径影响极小。这种状态最让人恼火——一切正常但你要的扩展点就是不执行。对比一下自绘CListBox时只要设了LBS_OWNERDRAWFIXEDDrawItem立刻被疯狂调用而CListCtrl设了LVS_OWNERDRAWFIXED却像什么都没发生一样。1.3 网上常见的误导性建议就这个问题我见过太多不靠谱的回复。有人说LVS_OWNERDRAWFIXED必须在Create时通过dwStyle传入运行时再改无效——我试了依然不触发。有人说ON_WM_MEASUREITEM_REFLECT这类反射消息只能在控件的消息映射里放在特定位置——这纯粹是臆测MFC的消息映射宏跟顺序没有这种强绑定关系。还有人说要重写WindowProc去拿原始消息——这种思路本身没错但它道出了一个真相指望CListCtrl像CListBox那样自动把绘制消息反射回派生类是行不通的。2. 三路排查从样式、消息、反射逐层定位根因2.1 第一路验证控件样式是否真的生效我第一步做的是确认LVS_OWNERDRAWFIXED到底有没有设置成功。在PreSubclassWindow里加断点查看GetWindowLong的返回值DWORD dwOldStyle GetWindowLong(GetSafeHwnd(), GWL_STYLE); DWORD dwNewStyle dwOldStyle | LVS_OWNERDRAWFIXED; SetWindowLong(GetSafeHwnd(), GWL_STYLE, dwNewStyle); DWORD dwVerify GetWindowLong(GetSafeHwnd(), GWL_STYLE); // dwVerify LVS_OWNERDRAWFIXED 确实不为0实测下来样式确实设置成功了。问题不在样式这一层。然后我顺手检查了视图模式。CListCtrl只有在Report视图下LVS_OWNERDRAWFIXED才被文档认为是“可能有效”的Icon、SmallIcon、List视图下基本不参与Owner Draw这套逻辑。我的控件用的是LVS_REPORT视图没问题。2.2 第二路确认WM_DRAWITEM是否发给了父窗口样式有了接下来要确认消息本身有没有发出。最直接的办法是在父窗口比如CDialog派生类里重写OnDrawItem和OnMeasureItem看这两条消息到底有没有到达父窗口// 父窗口消息映射 BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx) ON_WM_DRAWITEM() ON_WM_MEASUREITEM() END_MESSAGE_MAP() void CMyDialog::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct) { // 断点在这里 CDialogEx::OnDrawItem(nIDCtl, lpDrawItemStruct); } void CMyDialog::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct) { // 断点在这里 CDialogEx::OnMeasureItem(nIDCtl, lpMeasureItemStruct); }结果很有意思在默认视觉主题下WM_MEASUREITEM根本没有发送到父窗口WM_DRAWITEM同样没有任何动静。整个Owner Draw消息链路对CListCtrl来说就像不存在一样。为了进一步确认我临时禁用了视觉主题调用SetWindowTheme(GetSafeHwnd(), L, L)关闭主题对窗口的接管再跑一次。这时WM_MEASUREITEM和WM_DRAWITEM开始出现了。这基本验证了一个方向在现代公共控件库comctl32 v6以及视觉主题开启的情况下CListCtrl的系统绘制是走主题引擎内部路径的LVS_OWNERDRAWFIXED并不能保证触发WM_DRAWITEM。网上那些老教程能跑通往往是在XP时代或者关闭主题的环境下。2.3 第三路理解MFC反射为什么会失效如果你恰好关闭了主题WM_DRAWITEM确实发到了父窗口但派生类的ON_WM_DRAWITEM_REFLECT依然是“看运气”触发。因为MFC的反射机制是这样的控件发送WM_DRAWITEM给父窗口。父窗口的CWnd::OnDrawItem收到消息后通过lpDrawItemStruct-hwndItem找到发送消息的控件句柄。如果这个句柄对应一个由MFC管理且已Subclass的CWnd对象MFC会向该控件再发送一条OCM_DRAWITEM即WM_DRAWITEM OCM__BASE偏移。控件的消息映射里的ON_WM_DRAWITEM_REFLECT真正处理的其实是OCM_DRAWITEM而不是WM_DRAWITEM。CListBox这套走得通是因为它老老实实发WM_DRAWITEM给父窗口父窗口能通过hwndItem反射回控件自身。而CListCtrl在主题模式下根本不发WM_DRAWITEM反射自然无从谈起。至于WM_MEASUREITEM情况更微妙。CListCtrl即使发了WM_MEASUREITEMlParam指向的MEASUREITEMSTRUCT里的CtlID字段也不一定是控件的资源ID父窗口的CWnd::OnMeasureItem想通过CtlID去GetDlgItem查找控件经常找不到反射也就静默失败了。2.4 排查结论别在这条路上硬刚走完这三路结论已经很清楚了。CListCtrl的自绘优先使用的不是Owner Draw而是Custom Draw。NM_CUSTOMDRAW通知是它在绘制过程中主动发给父窗口的也天然支持MFC的ON_NOTIFY_REFLECT反射。与其研究怎么让LVS_OWNERDRAWFIXED和WM_DRAWITEM在CListCtrl上“复活”不如直接切换到它真正支持的自绘通道。3. 原理剖析CListCtrl的自绘和CListBox根本不是一回事3.1 Owner Draw的本质与适用控件Owner Draw这套机制核心是两条消息WM_MEASUREITEM负责告诉系统“每个条目的尺寸是多少”WM_DRAWITEM负责“每个条目怎么画”。操作系统把这两件事交给控件所有者通常是父窗口去处理。这套机制在CButton、CComboBox、CListBox、CMenu上都很好用。原因是这些控件的条目结构简单绘制需求统一系统可以很干脆地说“这条item我不画了有本事你画”。而CListCtrl不行它本身是一个复合控件有列头、有行、有单元格还有图标、状态、排序箭头等大量内部元素。如果在Report视图下把整行绘制权完全交给外部系统内部的命中测试、焦点矩形、键盘导航、编辑框定位等一大堆逻辑都会受影响。所以微软在设计list view时把详细的绘制过程做成了“分阶段回调”也就是NM_CUSTOMDRAW。3.2 CListCtrl真正的自绘通道NM_CUSTOMDRAWNM_CUSTOMDRAW的机制就像一个层层递进的流水线。控件在绘制的不同阶段都会发一条WM_NOTIFY给父窗口父窗口根据返回值决定下一步怎么做。大概分这几层CDDS_PREPAINT控件准备开始绘制此时你可以决定是否要关注条目级绘制。CDDS_ITEMPREPAINT某个条目行准备开始绘制。CDDS_SUBITEM | CDDS_ITEMPREPAINT某个单元格准备开始绘制。后置阶段CDDS_ITEMPOSTPAINT、CDDS_SUBITEM | CDDS_ITEMPOSTPAINT等。每一层的返回值控制后续流程。比如CDDS_PREPAINT返回CDRF_NOTIFYITEMDRAW后续才会收到ITEMPREPAINTITEMPREPAINT返回CDRF_NOTIFYSUBITEMDRAW后续才会收到每个单元格的绘制通知。这套机制最大的优点是不强制你画全部内容你可以只改颜色甚至只改某个单元格其余交给系统默认绘制。3.3 行高为什么也不该用MeasureItem很多人在CListBox里用MeasureItem设置条目动态高度到了CListCtrl发现没反应。原因在于CListCtrl在Report视图下的行高是“全局一致”的它根本没有为每个条目单独测量高度的概念。行高的正确设置方式是LVM_SETITEMHEIGHT消息对应的MFC封装是// 设置行高为28像素 m_list.SetItemHeight(0, 28);顺带一提WPARAM在这里通常传0LPARAM传像素值。这个设置是立即生效的不需要LVS_OWNERDRAWFIXED也不需要WM_MEASUREITEM。CListCtrl的局限也在这你没法做到第1行高30、第2行高50这种ListBox才能实现的动态高度。3.4 一张表看懂ListBox和ListCtrl的差异对比点CListBoxCListCtrlReport视图自绘核心消息WM_MEASUREITEM / WM_DRAWITEMNM_CUSTOMDRAW自绘相关样式LBS_OWNERDRAWFIXED / VARIABLELVS_OWNERDRAWFIXED受主题影响大行高设置MeasureItem里返回itemHeightLVM_SETITEMHEIGHT单元格级自绘不支持DrawItem自己算位置CDDS_SUBITEM阶段天然支持视觉主题兼容性较好Owner Draw差Custom Draw好绘制工作量必须画整项可只改颜色也可全自绘MFC反射支持ON_WM_DRAWITEM_REFLECT稳定ON_NOTIFY_REFLECT(NM_CUSTOMDRAW)稳定这个表基本就是我这几天踩坑后的浓缩版。以后凡是涉及CListCtrl美化直接跳过左上角那一列。4. 绕坑正道用NM_CUSTOMDRAW自绘CListCtrl的完整方案4.1 派生类中的消息映射与基本骨架推荐方案是在CListCtrl派生类里直接处理NM_CUSTOMDRAW这样绘制逻辑可以完全封装在控件类内部对话框不用介入。消息映射写法如下// CMyListCtrl.h class CMyListCtrl : public CListCtrl { protected: afx_msg void OnNmCustomDraw(NMHDR* pNMHDR, LRESULT* pResult); DECLARE_MESSAGE_MAP() };// CMyListCtrl.cpp BEGIN_MESSAGE_MAP(CMyListCtrl, CListCtrl) ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, CMyListCtrl::OnNmCustomDraw) END_MESSAGE_MAP() void CMyListCtrl::OnNmCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { NMLVCUSTOMDRAW* pLVCD reinterpret_castNMLVCUSTOMDRAW*(pNMHDR); DWORD dwDrawStage pLVCD-nmcd.dwDrawStage; // 先给默认值 *pResult CDRF_DODEFAULT; if (dwDrawStage CDDS_PREPAINT) { // 控件即将绘制告诉控件我需要逐条item绘制通知 *pResult CDRF_NOTIFYITEMDRAW; return; } if (dwDrawStage CDDS_ITEMPREPAINT) { // 某一行即将绘制这里可以对整行做颜色或字体调整 // 如果需要进一步细化到单元格则请求subitem通知 *pResult CDRF_NOTIFYSUBITEMDRAW; return; } if (dwDrawStage (CDDS_SUBITEM | CDDS_ITEMPREPAINT)) { // 某个单元格即将绘制 // 在这里做具体列的样式控制 return; } }注意消息映射用的是ON_NOTIFY_REFLECT不是ON_NOTIFY。这个反射宏会把父窗口收到的WM_NOTIFY再转回控件自身和之前失效的ON_WM_DRAWITEM_REFLECT完全是两条路径。实践下来只要控件是MFC派生类且正确Subclass了NM_CUSTOMDRAW的反射非常稳定。4.2 实现斑马纹按行设置背景色最常见的需求是隔行变色。在CDDS_ITEMPREPAINT阶段通过NMLVCUSTOMDRAW的clrText和clrTextBk字段可以控制整行的文字和背景颜色if (dwDrawStage CDDS_ITEMPREPAINT) { int nItem static_castint(pLVCD-nmcd.dwItemSpec); if (nItem % 2 0) { pLVCD-clrText RGB(32, 32, 32); pLVCD-clrTextBk RGB(244, 246, 248); } else { pLVCD-clrText RGB(32, 32, 32); pLVCD-clrTextBk RGB(255, 255, 255); } *pResult CDRF_NOTIFYSUBITEMDRAW; return; }这种写法只向系统提交颜色修改文字内容、图标、选中态仍然由系统绘制效率和观感都很好。如果你发现修改后文字和背景的颜色没有完全生效多半是扩展样式里没开LVS_EX_DOUBLEBUFFER或者列没有背景填充可以给控件加上LVS_EX_DOUBLEBUFFER试试。4.3 按单元格绘制给特定列加状态色有时候需要给某一列单独上色比如状态列红色表示异常这就要在CDDS_SUBITEM | CDDS_ITEMPREPAINT阶段处理。先获取当前单元格的行和列if (dwDrawStage (CDDS_SUBITEM | CDDS_ITEMPREPAINT)) { int nItem static_castint(pLVCD-nmcd.dwItemSpec); int nSubItem pLVCD-iSubItem; if (nSubItem 1) // 假设第2列是状态列 { CString strStatus GetItemText(nItem, nSubItem); if (strStatus _T(异常)) { pLVCD-clrText RGB(255, 255, 255); pLVCD-clrTextBk RGB(220, 80, 70); } else if (strStatus _T(正常)) { pLVCD-clrText RGB(32, 32, 32); pLVCD-clrTextBk RGB(90, 200, 120); } } return; }与整行不同单元格阶段可以拿到iSubItem所以控制粒度更细。返回CDRF_DODEFAULT之后系统会按最新设置的颜色继续绘制默认文本这样你不需要自己DrawText也不会破坏列的图标和排序箭头。4.4 全自绘单元格自己控制绘制内容颜色控制满足不了所有场景比如想在某个单元格里画一个小进度条或者画一个圆形状态灯。这时候就需要真正的“接管绘制”用CDRF_SKIPDEFAULT告诉系统这个单元格你别画了我自己来if (dwDrawStage (CDDS_SUBITEM | CDDS_ITEMPREPAINT)) { int nItem static_castint(pLVCD-nmcd.dwItemSpec); int nSubItem pLVCD-iSubItem; if (nSubItem 2) // 某列自绘 { CDC* pDC CDC::FromHandle(pLVCD-nmcd.hdc); CRect rcItem; GetSubItemRect(nItem, nSubItem, LVIR_BOUNDS, rcItem); // 画背景 pDC-FillSolidRect(rcItem, RGB(255, 255, 255)); // 画一个简单的进度条 CString strProgress GetItemText(nItem, nSubItem); int nProgress _ttoi(strProgress); CRect rcBar rcItem; rcBar.DeflateRect(4, 6, 4, 6); pDC-FillSolidRect(rcBar, RGB(230, 230, 230)); if (nProgress 0) { CRect rcFill rcBar; rcFill.right rcBar.left (rcBar.Width() * nProgress) / 100; pDC-FillSolidRect(rcFill, RGB(0, 160, 233)); } // 画进度文字 pDC-SetBkMode(TRANSPARENT); pDC-DrawText(strProgress, rcItem, DT_CENTER | DT_VCENTER | DT_SINGLELINE); *pResult CDRF_SKIPDEFAULT; return; } }这里有个容易踩的坑CDRF_SKIPDEFAULT是“完全不画”包括背景、文字、焦点框都不画所以你必须自己处理完整。如果只画了背景和文字但忘了边框视觉上会少东西。另外GetSubItemRect用之前先确认控件设置了LVS_EX_FULLROWSELECT或者至少是Report视图否则第0列的矩形会覆盖整行宽度导致绘制错位。4.5 配套行高、整行选中与防闪烁在对话框初始化时建议统一做好这些设置m_list.SetExtendedStyle( LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER); // 行高28像素 ListView_SetItemHeight(m_list.GetSafeHwnd(), 0, 28); // 加列和设置列宽 m_list.InsertColumn(0, _T(名称), LVCFMT_LEFT, 120); m_list.InsertColumn(1, _T(状态), LVCFMT_LEFT, 80); m_list.InsertColumn(2, _T(进度), LVCFMT_LEFT, 100);LVS_EX_DOUBLEBUFFER非常关键。CListCtrl在Custom Draw过程中频繁触发刷新没有双缓冲的话滚动时会出现明显闪烁。LVS_EX_FULLROWSELECT则让点击任意列都能选中整行对自绘观感影响很大。4.6 Custom Draw返回值速查返回值含义使用场景CDRF_DODEFAULT按系统默认方式继续只修改了颜色或不需要特殊处理CDRF_SKIPDEFAULT不再执行默认绘制完全自绘单元格或整行CDRF_NOTIFYITEMDRAW要求后续发送条目级通知在CDDS_PREPAINT返回CDRF_NOTIFYSUBITEMDRAW要求后续发送单元格级通知在CDDS_ITEMPREPAINT返回CDRF_NOTIFYPOSTPAINT条目绘制完成后发送通知需要画选中框、高亮线等CDRF_NEWFONT已改变字体系统需要重新计算在ITEMPREPAINT中SelectObject字体后返回记住一个口诀PREPAINT要ITEMPREPAINT就返回NOTIFYITEMDRAWITEMPREPAINT要单元格就返回NOTIFYSUBITEMDRAW真不想让系统画就返回SKIPDEFAULT只是改色就返回DODEFAULT。5. 非要用DrawItem父窗口Owner Draw的抢救与局限5.1 在父窗口直接处理WM_DRAWITEM如果你接手的老代码已经把绘制逻辑写在DrawItem里暂时改不动那也不是完全没救。一个可行的办法是放弃在CListCtrl派生类里放DrawItem改到父窗口里处理。在对话框或视图类的消息映射加BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx) ON_WM_DRAWITEM() END_MESSAGE_MAP() void CMyDialog::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct) { if (nIDCtl IDC_MYLIST) { // 从LPDRAWITEMSTRUCT中拿到绘制区域和item数据 CDC* pDC CDC::FromHandle(lpDrawItemStruct-hDC); CRect rcItem(lpDrawItemStruct-rcItem); // 画背景 pDC-FillSolidRect(rcItem, RGB(255, 255, 255)); // 画文字 pDC-SetBkMode(TRANSPARENT); pDC-DrawText(_T(自定义内容), rcItem, DT_LEFT | DT_VCENTER | DT_SINGLELINE); return; } CDialogEx::OnDrawItem(nIDCtl, lpDrawItemStruct); }但这里有个前提前面已经强调过WM_DRAWITEM对CListCtrl而言在开启视觉主题的现代系统上经常不发送。所以这个方案能用但不一定在任何环境都能触发。如果你只是改颜色、改行高这个方案远不如Custom Draw可靠。5.2 让反射生效的冷门条件真要在派生类里让ON_WM_DRAWITEM_REFLECT跑起来你需要满足相当多条件。我总结过至少包括控件必须是MFC管理窗口并且经过了SubclassWindow或DDX_Control绑定不是临时包装的HWND。视图模式必须是LVS_REPORT同时设置了LVS_OWNERDRAWFIXED。当前系统不能使用WinXP之后的视觉主题接管绘制路径必要时调用SetWindowTheme关闭主题。父窗口收到WM_DRAWITEM后CWnd::OnDrawItem才能正确按hwndItem反射回控件如果某层逻辑提前消费了消息反射就断掉。你需要在派生类中同时保留ON_WM_DRAWITEM_REFLECT且不能和父窗口的ON_WM_DRAWITEM冲突。这些条件凑齐的难度比你直接迁移到NM_CUSTOMDRAW高得多。如果你不是被历史代码锁死我强烈建议不要在反射这里浪费时间。5.3 为什么不推荐这条路一句话总结CListCtrl的Owner Draw路径是历史遗留设计在现代系统上处于“时灵时不灵”的状态。你花一晚上让它跑起来换一台用户机器、换一个Windows版本可能又失效了。Custom Draw则是list view官方推荐的自绘方式从XP到Win11一路兼容。两条路投入产出比完全不对等。6. 常见问题速查与避坑实录6.1 问题排查速查表症状可能原因应对方案ON_WM_MEASUREITEM_REFLECT不触发系统未发WM_MEASUREITEM或者CtlID无法定位到CListCtrl行高改用SetItemHeight不依赖MeasureItemDrawItem不触发主题模式不发送WM_DRAWITEM或反射链路中断改用NM_CUSTOMDRAW或在父窗口处理WM_DRAWITEM并注意环境限制NM_CUSTOMDRAW不触发没用ON_NOTIFY_REFLECT或消息被父窗口OnNotify拦截确认宏是ON_NOTIFY_REFLECT检查父窗口OnNotify是否提前返回修改clrTextBk不生效没有设置LVS_EX_DOUBLEBUFFER或列背景被系统覆盖加上LVS_EX_DOUBLEBUFFER扩展样式自绘单元格后文字错位GetSubItemRect获取的矩形和实际绘制有偏移区分第0列和后续列使用LVIR_BOUNDS并配合LVS_EX_FULLROWSELECT滚动时闪烁自绘逻辑频繁触发重绘没有双缓冲启用LVS_EX_DOUBLEBUFFER减少双缓冲DC之外的自绘面积6.2 调试自绘的实用技巧排查自绘问题我习惯在OnNmCustomDraw入口先打一个条件断点条件是dwDrawStage CDDS_PREPAINT。只要能在这里停下来说明通知链路是通的后续阶段只是返回值的问题。如果PREPAINT都进不来那就查控件是否是MFC子类、是否正常Subclass、消息映射是否真的挂上了。另外可以用Spy或者SetWindowLong配合GetWindowLong观察父窗口收到的WM_NOTIFY通知码确认NM_CUSTOMDRAW确实被发送。很多时候“不触发”不是系统没发而是你的派生类根本没被正确的消息映射处理或者你同时拦了父窗口的OnNotify并且提前return了。6.3 一个容易忽略的坑第0列的特殊性在Report视图下第0列也就是最左列和其它列有一个明显区别它承载着整行的选择和展开状态。当控件没有设置LVS_EX_FULLROWSELECT时第0列的宽度会延伸到整个客户区后面的列只是在上面“覆盖”绘制。这种情况下你在CDDS_SUBITEM阶段获取第0列的GetSubItemRect经常得到一个宽得离谱的矩形。解决方法是要么设置LVS_EX_FULLROWSELECT让行选择贯穿所有列要么在自绘时对第0列单独做矩形裁剪。我建议直接开启LVS_EX_FULLROWSELECT不仅视觉统一自绘逻辑也简单很多。6.4 关于Custom Draw的字体修改在CDDS_ITEMPREPAINT阶段你可以选择一个新的字体让整行文字换字体。关键是要在选中字体后返回CDRF_NEWFONT系统才会重新度量并采用你的字体if (dwDrawStage CDDS_ITEMPREPAINT) { CDC* pDC CDC::FromHandle(pLVCD-nmcd.hdc); CFont* pOldFont pDC-SelectObject(m_FontBold); // 注意SelectObject后不需要立即恢复系统绘制完会自动处理 *pResult CDRF_NOTIFYSUBITEMDRAW | CDRF_NEWFONT; return; }不返回CDRF_NEWFONT的常见现象是你看代码里SelectObject了绘制结果却还是旧字体。这个返回值专门就是告诉控件“字体变了按新字体重新排版”。类似的如果你在某些阶段修改了HDC的映射模式或者画刷也要考虑返回对应的标志否则系统默认绘制可能不认你的新设置。6.5 和虚拟列表LVS_OWNERDATA共存的注意事项如果你用了虚拟列表LVS_OWNERDATA自绘时注意一点NM_CUSTOMDRAW照常工作你可以给任意行和单元格加颜色但不要试图在DrawItem里获取不存在的文本。虚拟列表模式下系统不保存文本数据GetItemText返回的往往是空字符串或者是你通过LVN_GETDISPINFO动态提供的内容。我在自绘状态列时就吃过这个亏直接get文本得到空串最后改成在LVN_GETDISPINFO里先准备一份缓存再在Custom Draw阶段读取缓存问题才解决。我的体会是自绘CListCtrl这件事只要方向对了半小时就能出效果方向错了能折腾好几天。希望这篇文章能让你少走这段弯路。最后再分享一个小技巧调试Custom Draw时把dwDrawStage的所有阶段都用OutputDebugString打印一遍你会看到系统绘制ListView时其实经过了非常多的阶段回调理解每个阶段的意图之后画什么都能做到游刃有余。