news 2026/10/6 3:39:37

自绘CListCtrl的常见误区:从Owner Draw到NM_CUSTOMDRAW的正确切换

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
自绘CListCtrl的常见误区:从Owner Draw到NM_CUSTOMDRAW的正确切换

做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_OWNERDRAWFIXED,DrawItem立刻被疯狂调用;而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_MEASUREITEM,lParam指向的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_CUSTOMDRAW

NM_CUSTOMDRAW的机制就像一个层层递进的流水线。控件在绘制的不同阶段,都会发一条WM_NOTIFY给父窗口,父窗口根据返回值决定下一步怎么做。大概分这几层:

  • CDDS_PREPAINT:控件准备开始绘制,此时你可以决定是否要关注条目级绘制。
  • CDDS_ITEMPREPAINT:某个条目(行)准备开始绘制。
  • CDDS_SUBITEM | CDDS_ITEMPREPAINT:某个单元格准备开始绘制。
  • 后置阶段:CDDS_ITEMPOSTPAINT、CDDS_SUBITEM | CDDS_ITEMPOSTPAINT等。

每一层的返回值控制后续流程。比如CDDS_PREPAINT返回CDRF_NOTIFYITEMDRAW,后续才会收到ITEMPREPAINT;ITEMPREPAINT返回CDRF_NOTIFYSUBITEMDRAW,后续才会收到每个单元格的绘制通知。这套机制最大的优点是不强制你画全部内容,你可以只改颜色,甚至只改某个单元格,其余交给系统默认绘制。

3.3 行高为什么也不该用MeasureItem

很多人在CListBox里用MeasureItem设置条目动态高度,到了CListCtrl发现没反应。原因在于CListCtrl在Report视图下的行高是“全局一致”的,它根本没有为每个条目单独测量高度的概念。行高的正确设置方式是LVM_SETITEMHEIGHT消息,对应的MFC封装是:

// 设置行高为28像素 m_list.SetItemHeight(0, 28);

顺带一提,WPARAM在这里通常传0,LPARAM传像素值。这个设置是立即生效的,不需要LVS_OWNERDRAWFIXED,也不需要WM_MEASUREITEM。CListCtrl的局限也在这:你没法做到第1行高30、第2行高50这种ListBox才能实现的动态高度。

3.4 一张表看懂ListBox和ListCtrl的差异

对比点CListBoxCListCtrl(Report视图)
自绘核心消息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_cast<NMLVCUSTOMDRAW*>(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_cast<int>(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_cast<int>(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_cast<int>(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就返回NOTIFYITEMDRAW,ITEMPREPAINT要单元格就返回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,不依赖MeasureItem
DrawItem不触发主题模式不发送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时其实经过了非常多的阶段回调,理解每个阶段的意图之后,画什么都能做到游刃有余。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 3:39:36

JProfiler 8.0.2 Windows x64安装与Java性能分析入门实战

JProfiler_windows-x64_8_0_2 这个安装包&#xff0c;我在Windows机器上装过不下十次了&#xff0c;从个人开发机到团队的测试服务器&#xff0c;基本都是同一个套路&#xff1a;双击exe、配许可证、连上Java进程、开分析。它是我在Java性能分析这个方向上用得最多、也最愿意推…

作者头像 李华
网站建设 2026/10/6 3:39:26

Lasso超参数调整与模型选择:L1稀疏原理到sklearn实践

做机器学习的人大概都遇到过这种场景&#xff1a;手里一张宽表&#xff0c;几十个特征&#xff0c;业务方拍着胸脯说"每一个都有业务含义"&#xff0c;可真跑起线性回归来&#xff0c;要么系数奇奇怪怪&#xff0c;要么测试集一验证就崩。这种时候Lasso就是绕不开的选…

作者头像 李华
网站建设 2026/10/6 3:39:12

无感FOC核心算法:龙伯格观测器原理、离散化与参数整定全解析

1. 无感FOC里为什么绕不开状态观测器做无感FOC控制&#xff0c;核心问题就一个&#xff1a;转子位置和速度怎么拿。装编码器或霍尔&#xff0c;成本上去了&#xff0c;而且很多场景根本装不下。所以行业内主流方案是走无感路线——不装位置传感器&#xff0c;靠电机的电压电流反…

作者头像 李华
网站建设 2026/10/6 3:38:58

许三观卖血记:男人的爱不是低三下四,而是关键时刻挺身而出

读《许三观卖血记》是很多年前的事了&#xff0c;但书中那些密密麻麻的细节&#xff0c;像许三观弓着背坐在门槛上喝黄酒的样子、许玉兰站在街口数落他的声音&#xff0c;一直留在我脑子里。后来我把这本书重读了两遍&#xff0c;越读越觉得&#xff0c;余华写这个故事&#xf…

作者头像 李华
网站建设 2026/10/6 3:37:48

椭圆曲线密码学(ECC)从原理到实践:ECDH、ECDSA与工程避坑指南

1. 为什么偏偏是椭圆曲线&#xff1a;公钥密码的必然选择聊到现代密码学&#xff0c;绕不开一个核心场景&#xff1a;两个从未见过面的人&#xff0c;怎么在不安全的信道上安全地交换密钥、验证身份&#xff1f;从上世纪七十年代 Diffie-Hellman 密钥交换出现以来&#xff0c;这…

作者头像 李华