ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MFC重绘CListCtrl三件套:自绘滚动条、表头与列表项的工程方案

MFC重绘CListCtrl三件套:自绘滚动条、表头与列表项的工程方案 简介面向MFC界面开发者的CListCtrl深度定制资料聚焦滚动条、表头与列表项三重重绘适合需要为类似资源管理器的列表界面打造个性化视觉风格的场景。代码按三步展开先继承CScrollBar并重写OnPaint绘制滚动条轨道与滑块再通过自定义CHeaderCtrl实现表头字体与配色最后借LVS_OWNERDRAWFIXED样式与OnDrawItem重载控制每个列表项的绘制并给出WM_HSCROLL、WM_VSCROLL、HDM_LAYOUT等消息的处理思路。压缩包共42个文件以bmp位图资源14个、h头文件9个和cpp源码7个为主体辅以txt说明与工程配置体积仅87KB。内含可运行的SkinList_demo示例工程边看代码边对照试验能直观感受重绘前后差异。已有326人学习浏览适合具备MFC基础、正准备提升控件定制能力的开发者作为实战参考。1. 重绘CListCtrl的scrollbar、headerctrl和items一次把列表控件三件套改造到能看的工程方案接手一个用MFC写的老项目最头疼的往往是那些默认控件的“原生脸”。CListCtrl这种列表控件Windows 7时代还算顺眼放到Windows 10/11的深色主题产品里系统滚动条灰得发亮、表头白底黑字、选中项蓝得刺眼整个界面像打了补丁。要把CListCtrl彻底重绘绕不开三块scrollbar、headerctrl、items。很多人的做法是只给items上色结果滚动条和表头照样辣眼睛半吊子还不如不改。这篇笔记把我实际落地过的一套方案讲清楚三个部件分别走什么绘制机制、每个部件怎么动手、参数怎么调、哪些地方容易翻车。适合正在维护MFC老工程、被Win10/11视觉风格逼着改UI的C工程师也适合刚接触自绘控件、想知道从哪下手的初学者。方案核心是让列表控件三个部件的绘制权全部收回到你手里而不是去跟系统主题搏斗。2. 先定框架自绘CListCtrl的滚动条、表头和列表项都有各自的绘制机制2.1 三件套对应的绘制入口ScrollBar、HeaderCtrl、NM_CUSTOMDRAW重绘之前先把CListCtrl内部结构摸清楚。列表控件不是一个简简单单的窗口它至少包含三样东西一个拥有客户区的主体窗口、一个CHeaderCtrl表头子控件、以及由系统负责绘制的内容区域。滚动条情况特殊一点当列表使用标准系统滚动条时它画在非客户区没有独立窗口句柄只有启用FlatSB或者自己替换滚动条才有真正意义上的“窗口”可以操作。三件套的绘制机制差别很大不能用一套代码通吃部件本质可用的自绘入口scrollbar非客户区或独立子窗口FlatSB颜色接口 / 自绘CScrollBar替换headerctrlCListCtrl的子控件HDS_OWNERDRAWN DrawItemitemsCListCtrl主体自行渲染NM_CUSTOMDRAW通知反射理解这个表格你就明白了为什么很多人卡住items用的是Custom Draw机制headerctrl用的是Owner Draw机制scrollbar又是另一套。三套机制各自独立依赖的消息、回调函数、返回码都不同。所谓“重绘CListCtrl”本质是同时驱动这三套机制让它们在一棵列表里风格统一。2.2 一个边界重绘items不是重写OnPaint有个常见误区——为了自绘列表项直接重写CListCtrl的OnPaint或OnEraseBkgnd把客户的绘制逻辑全塞进去。这个思路在简单场景下能跑但列表一旦涉及整行选中、聚焦矩形、热追踪、状态图标系统内部已经做了很多状态管理OnPaint里很难拿到每行每列的真实状态越写越像在跟黑匣子搏斗。Windows列表控件提供了一套相对友好的扩展机制NM_CUSTOMDRAW。它会按绘制阶段把消息反射给控件本身你在每个阶段决定“系统画还是自己画”。这套机制好处在于你不必接管整个绘制流程只要在合适时机截住某一层的绘制改完再放行。缺点是阶段码比较多新手一上来容易被CDDS_PREPAINT、CDDS_ITEMPREPAINT、CDDS_SUBITEMPREPAINT这种名词绕晕。但只要把阶段顺序理清这套机制比OnPaint可控得多。2.3 派生类骨架一个CMyListCtrl把三部分串起来实际操作中我一般从CListCtrl派生一个类把滚动条、表头、列表项三个子对象都封装进去这样DDX_Control绑定后所有自绘逻辑就自动生效。类的声明大概长这样// MyListCtrl.h class CMyListCtrl : public CListCtrl { public: DECLARE_DYNAMIC(CMyListCtrl) protected: CMyScrollBar m_wndScrollBar; // 自绘滚动条替代系统滚动条 CMyHeaderCtrl m_wndHeader; // 自绘表头 public: void ApplyTheme(const ListTheme theme); // 统一设置三个部件颜色 protected: afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); afx_msg void OnNMCustomDraw(NMHDR* pNMHDR, LRESULT* pResult); DECLARE_MESSAGE_MAP() };这个骨架的关键点在于滚动条用成员对象持有而不是每次用时再找句柄表头也要子类化items通过消息映射里的ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, OnNMCustomDraw)反射进这个类。后续所有颜色、字体、尺寸配置统一走ApplyTheme避免三个部件各写各的导致风格漂移。3. 重绘scrollbar替换成自绘滚动条是最稳妥的落地路径3.1 为什么直接画系统滚动条是个伪命题CListCtrl默认的系统滚动条不是独立控件窗口它绘制在非客户区没有HWND也没有WM_PAINT可以重写。有人用WM_NCPAINT去画但非客户区绘制要自己处理边框、系统按钮、滚动条状态代码极其脆弱主题一换就对不齐。常见的快捷做法是用FlatSB API给滚动条换色比如调用FlatSB_EnableScrollBar之后用FlatSB_SetScrollProp设置track和thumb的颜色。但这里有个大坑当代应用程序清单里几乎都启用了ComCtl32 v6视觉样式FlatSB的颜色设置在视觉样式激活时会被系统主题覆盖设置了个寂寞。所以我在项目里不把FlatSB当终点只当临时救急手段。3.2 稳妥路径隐藏系统滚动条用自绘CScrollBar替代我最终采用的方案是关掉CListCtrl的系统滚动条在列表客户区右侧放一个自绘的CScrollBar子窗口自己维护它的SCROLLINFO并在滚动时把位置同步回列表。这样做的好处是滚动条完全受你控制颜色、宽度、圆角、箭头全部自己画不再受视觉样式干扰。先在CMyListCtrl的OnCreate或PreSubclassWindow里禁用系统滚动条int CMyListCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CListCtrl::OnCreate(lpCreateStruct) -1) return -1; // 关掉系统滚动条避免和自绘滚动条重叠 ShowScrollBar(SB_BOTH, FALSE); // 创建自绘滚动条子窗口贴在右侧客户区 m_wndScrollBar.Create(WS_CHILD | WS_VISIBLE | SBS_VERT, CRect(0, 0, 14, 100), this, 1001); UpdateScrollBarInfo(); return 0; }这里有两个细节ShowScrollBar(SB_BOTH, FALSE)只是隐藏不是销毁所以列表的内部滚动逻辑仍存在但我们不再依赖它的视觉呈现自绘滚动条宽度我习惯用14px因为正好是默认系统垂直滚动条的宽度视觉上替换时不会有明显的跳动。如果你做更窄的滚动条记得同时调整其矩形位置。3.3 自绘滚动条的绘制track、thumb、箭头三步走CMyScrollBar的绘制集中在OnPaint里。我先画track背景再根据SCROLLINFO计算thumb位置和高度最后画上下箭头按钮。这里最核心的是thumb高度计算直接关系拖动手感void CMyScrollBar::OnPaint() { CPaintDC dc(this); CRect rc; GetClientRect(rc); // 1. 画track背景 dc.FillSolidRect(rc, m_clrTrack); // 2. 取滚动信息 SCROLLINFO si { sizeof(si), SIF_ALL }; GetScrollInfo(si); // 3. 计算thumb高度 // nPage是当前可见的“单位数”nMax-nMin1是总范围 double dPage (double)si.nPage; double dRange (double)(si.nMax - si.nMin 1); double ratio dRange 0 ? dPage / dRange : 1.0; if (ratio 1.0) ratio 1.0; // 内容不足一屏时不要算成负数 int nThumbH max(24, (int)(rc.Height() * ratio)); // 4. 计算thumb位置 int nScrollable si.nMax - si.nMin - si.nPage 1; double dPos nScrollable 0 ? (double)(si.nPos - si.nMin) / nScrollable : 0.0; int nThumbY rc.top (int)((rc.Height() - nThumbH) * dPos); // 5. 画thumb CRect rcThumb(rc.left, nThumbY, rc.right, nThumbY nThumbH); dc.FillSolidRect(rcThumb, m_clrThumb); }这个OnPaint里有三个参数值得你细调。第一thumb最小高度24px小于这个值鼠标很难精准命中第二nPage不能直接用像素行数要用SCROLLINFO里的逻辑单位通常是行数或项数第三ratio大于1时说明内容不满一屏此时要强制ratio为1否则thumb会超出窗口底部。很多自绘滚动条翻车都是这三处边界没处理干净。3.4 滚动位置同步别让列表和滚动条各走各的自绘滚动条画出来之后还差一步联动。CListCtrl有自己的滚动逻辑需要把UI方向和实际位置对齐。做法是给CMyScrollBar加一个WM_VSCROLL回调在用户拖动thumb时把SCROLLINFO位置发给父窗口CMyListCtrl收到后再真正滚动列表内容。void CMyScrollBar::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { // 把用户操作转发给列表控件让它执行实际的滚动 GetParent()-SendMessage(WM_VSCROLL, MAKEWPARAM(nSBCode, nPos), (LPARAM)GetSafeHwnd()); } void CMyListCtrl::UpdateScrollBarInfo() { SCROLLINFO si { sizeof(si), SIF_ALL }; GetScrollInfo(SB_VERT, si); m_wndScrollBar.SetScrollInfo(si); m_wndScrollBar.Invalidate(); }这里最隐蔽的坑是当用户拖动thumb时CListCtrl内部会更新自己的SCROLLINFO但自绘滚动条不会自动刷新。所以列表滚动发生后必须调用一次UpdateScrollBarInfo把最新的SCROLLINFO推给滚动条。我一般会把几个触发点都挂上OnVScroll处理完后、OnMouseWheel处理完后、SetItemCount之后。漏掉任何一处滚动条thumb都会停在原地不动表现得像个僵尸——这是自绘滚动条最常见的“玄学故障”其实只是忘了同步。4. 重绘headerctrlGetHeaderCtrl拿到句柄后DrawItem才真正接管4.1 子类化表头句柄获取的正确姿势表头是CListCtrl的子控件有独立HWND所以处理它最直接的方式是子类化。在CMyListCtrl初始化时通过GetHeaderCtrl拿到指针再把它转交给CMyHeaderCtrl子类化void CMyListCtrl::PreSubclassWindow() { CListCtrl::PreSubclassWindow(); // 获取表头子控件句柄 HWND hHeader NULL; CHeaderCtrl* pHeader GetHeaderCtrl(); if (pHeader ::IsWindow(pHeader-GetSafeHwnd())) hHeader pHeader-GetSafeHwnd(); else hHeader ::GetDlgItem(m_hWnd, 0); // 兼容某些创建顺序 // 子类化表头必须在列表显示前完成 if (hHeader !m_wndHeader.GetSafeHwnd()) m_wndHeader.SubclassWindow(hHeader); }用GetHeaderCtrl之前先判空这个习惯很重要。CListCtrl在早期初始化阶段表头可能尚未创建完整GetHeaderCtrl返回空指针是常态。保底方案是GetDlgItem(0)这是列表表头固定的控件ID。子类化之后表头的绘制消息就会先经过CMyHeaderCtrl不再直接走系统默认逻辑。4.2 开启Owner DrawModifyStyle是关键一步拿到句柄还不够必须给表头窗口加上HDS_OWNERDRAWN风格它才会在每次绘制时回调DrawItem。在CMyHeaderCtrl的PreSubclassWindow里做这件事最合适void CMyHeaderCtrl::PreSubclassWindow() { CHeaderCtrl::PreSubclassWindow(); // 开启自绘同时去掉系统排序箭头的默认绘制 ModifyStyle(0, HDS_OWNERDRAWN); // 表头高度设为固定的24px配合深色背景 SetItemSize(24); }SetItemSize控制的是表头总高度配合HDS_FIXEDWIDTH风格会更稳定否则用户拖动列宽时表头高度可能跟着变。设成24px是深色UI里比较常见的基准值如果你界面里控件密度高22px更紧凑如果字号大、有图标28px更安全。4.3 DrawItem实现先画背景再画分隔线最后画文字和排序箭头CHeaderCtrl的自绘走的是DrawItem收到的是LPDRAWITEMSTRUCT。绘制顺序建议固定为背景、分隔线、文本、排序箭头。这个顺序不是随意的——先覆盖整块区域才不会残留上一步绘制的痕迹排序箭头最后画是为了让它永远在文本上层。void CMyHeaderCtrl::DrawItem(LPDRAWITEMSTRUCT lpDIS) { CDC* pDC CDC::FromHandle(lpDIS-hDC); CRect rcItem(lpDIS-rcItem); int nCol (int)lpDIS-itemID; // 1. 背景整列铺底色 pDC-FillSolidRect(rcItem, m_clrHeaderBack); // 2. 右侧分隔线1px左侧也画一条保持边界清晰 pDC-FillSolidRect(rcItem.right - 1, rcItem.top, 1, rcItem.Height(), m_clrHeaderBorder); // 3. 排序箭头有排序状态才绘制 TCHAR szText[128] {0}; HDITEM hdi {0}; hdi.mask HDI_TEXT | HDI_FORMAT; hdi.pszText szText; hdi.cchTextMax 127; GetItem(nCol, hdi); CRect rcText rcItem; rcText.left 6; if (hdi.fmt HDF_SORTUP) PaintArrow(pDC, rcText, true); else if (hdi.fmt HDF_SORTDOWN) PaintArrow(pDC, rcText, false); // 4. 文本靠左居中避免被排序箭头覆盖 rcText.right - 16; pDC-SetBkMode(TRANSPARENT); pDC-SetTextColor(m_clrHeaderText); pDC-DrawText(szText, rcText, DT_LEFT | DT_VCENTER | DT_SINGLELINE); }这个函数的几个参数决定表头观感文本左边距6px是为了让第一列文字不贴边右侧留16px因为右侧要留给排序箭头。如果你项目里排序列很多还要把排序箭头的绘制独立成PaintArrow函数用三角路径填充而不是画字符否则在不同字体下箭头位置会漂移。4.4 排序箭头状态同步点击列头后要主动刷新HDS_OWNERDRAWN有一个连带问题系统在列头点击排序时只是修改了内部排序状态并不会自动触发完整重绘。所以很多时候你会发现箭头只有一半、或者点完不出现得鼠标移开再回来才正常。原因是DrawItem里读取的HDF_SORTUP/HDF_SORTDOWN状态没在第一时间同步到绘制层。解决办法是拦截表头的HDN_ITEMCLICK通知在父窗口里收到该消息后强制Invalidate表头区域// CMyListCtrl的消息映射中 ON_NOTIFY(HDN_ITEMCLICKA, 0, CMyListCtrl::OnHeaderItemClick) ON_NOTIFY(HDN_ITEMCLICKW, 0, CMyListCtrl::OnHeaderItemClick) void CMyListCtrl::OnHeaderItemClick(NMHDR* pNMHDR, LRESULT* pResult) { m_wndHeader.Invalidate(); *pResult 0; }这里有个容易漏掉的小坑HDN_ITEMCLICK分ANSI和Unicode两个版本消息映射里两个都要写否则Unicode编译下点击列头不触发。反正我每次写表头点击通知都会顺手把这两个宏一起放上去省得后期忘了再补效果立竿见影。5. 重绘items用NM_CUSTOMDRAW三阶段搞定整行高亮和焦点框5.1 Custom Draw的消息反射注册时机和条件items的自绘走NM_CUSTOMDRAW它是由列表控件发出、最终反射到CMyListCtrl的通知消息。在CMyListCtrl的消息映射里注册BEGIN_MESSAGE_MAP(CMyListCtrl, CListCtrl) ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, CMyListCtrl::OnNMCustomDraw) END_MESSAGE_MAP()ON_NOTIFY_REFLECT是MFC里非常关键的一个宏它表示消息不是发给父窗口而是先反射给控件自身处理。用这个宏CMyListCtrl就能直接接管自己的绘制而不需要在对话框里写一堆通知处理。这也是把三件套封装到一个类里的好处对话框只管绑定CMyListCtrl所有自绘逻辑都隐形地生效了。5.2 绘制阶段解码PREPAINT、ITEMPREPAINT、SUBITEMPREPAINTCustom Draw的难点是阶段码。你收到的dwDrawStage决定当前该画什么常见的组合如下阶段码含义适合做的事CDDS_PREPAINT列表即将开始绘制告诉系统需要哪些阶段的通知CDDS_ITEMPREPAINT某一行即将绘制设置整行文字色、背景色CDDS_SUBITEMPREPAINT某个单元格即将绘制按列设置不同颜色CDDS_ITEMPOSTPAINT某一行绘制完毕画焦点矩形、自定义边框处理思路是在PREPAINT阶段请求ITEMPREPAINT和SUBITEMPREPAINT通知这样每一行、每一个单元格都会经过你的手然后再具体在行阶段或单元格阶段做定制。5.3 整行选中背景设置clrTextBk比手动填充更可靠很多新手追求“自己FillSolidRect整行”但系统在SubItem阶段还会叠加默认背景导致你画的高亮被盖掉一半。更稳的做法是直接告诉Custom Draw机制这一行的文字色和背景色让系统用你的颜色去画。void CMyListCtrl::OnNMCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { LPNMLVCUSTOMDRAW lpLVCD (LPNMLVCUSTOMDRAW)pNMHDR; DWORD dwStage lpLVCD-nmcd.dwDrawStage; if (dwStage CDDS_PREPAINT) { // 请求行和单元格两级通知 *pResult CDRF_NOTIFYITEMDRAW | CDRF_NOTIFYSUBITEMDRAW; return; } if (dwStage CDDS_ITEMPREPAINT) { // 整行状态选中行、普通行、禁用行自行区分 BOOL bSelected (lpLVCD-nmcd.uItemState CDIS_SELECTED); if (bSelected) { lpLVCD-clrText m_clrItemSelText; lpLVCD-clrTextBk m_clrItemSelBkg; } else { lpLVCD-clrText m_clrItemText; lpLVCD-clrTextBk m_clrItemBkg; } *pResult CDRF_NOTIFYSUBITEMDRAW; // 继续让单元格阶段通知 return; } if (dwStage CDDS_SUBITEMPREPAINT) { // 需要按列特殊处理的逻辑在这里写 *pResult CDRF_DODEFAULT; return; } *pResult CDRF_DODEFAULT; }这里有一个容易被忽略的彩蛋clrTextBk影响的是文字底色虽然看起来像整行背景但实际上系统只会在文字所在区域填充。如果你设置了LVS_EX_FULLROWSELECTWindows会帮你把整行拉通如果没设置背景色就只能盖住文字区域视觉上会一条一条的。所以记得给列表加上整行选中扩展样式SetExtendedStyle(GetExtendedStyle() | LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER);5.4 ITEMPREPAINT返回值的连锁反应新手最容易栽在返回值上。ITEMPREPAINT如果不返回CDRF_NOTIFYSUBITEMDRAW后面的单元格阶段根本不会触发按列定制就全部失效。反过来如果你返回CDRF_DODEFAULT系统又会忽略你已经修改的clrText和clrTextBk等于白改。正确的做法是行阶段修改属性后返回CDRF_NOTIFYSUBITEMDRAW让系统带着你的属性继续画后续单元格。另外CDRF_NOTIFYPOSTPAINT这个返回码也有用途。它告诉系统在整行绘制完成后再次通知你适合在行末尾补画边框、焦点框。我通常会在需要自绘分隔线的场景里用ITEMPOSTPAINT在行底画一条1px的分隔线比依赖系统的网格线更可控。5.5 键盘焦点矩形不画就会在选中态里消失列表支持键盘操控时焦点项和选中项是不同状态。选中项高亮很容易处理但焦点虚线矩形经常被忽略——字符在选中背景下被吞掉键盘导航的用户完全看不到光标在哪。解决方案是开启CDRF_NOTIFYITEMPOSTPAINT在行绘制结束后用DrawFocusRect补一个矩形if (dwStage CDDS_ITEMPOSTPAINT) { int nItem (int)lpLVCD-nmcd.dwItemSpec; if (GetFocus() GetSafeHwnd() (lpLVCD-nmcd.uItemState CDIS_FOCUS)) { CRect rcItem; if (GetItemRect(nItem, rcItem, LVIR_BOUNDS)) { CDC* pDC CDC::FromHandle(lpLVCD-nmcd.hdc); pDC-DrawFocusRect(rcItem); } } }注意条件里加了GetFocus() GetSafeHwnd()目的是只有列表自身拥有焦点时才画焦点框否则列表不在焦点态却画了虚线框界面会显得脏。这个细节也是后来被测试反复报bug才补上的——有个阶段列表失焦了焦点框还挂在最后一项上看着就像卡死了一样。6. 避坑自绘ListCtrl三件套最容易翻车的六个地方6.1 第一行文字变成蓝底白字自绘选中色没生效现象列表第一行或者正在编辑的单元格文字背景突然变回系统默认蓝和自绘的其他行颜色不一致。原因同时设置了LVS_EX_FULLROWSELECT但NM_CUSTOMDRAW的行阶段没有正确返回CDRF_NOTIFYSUBITEMDRAW导致subitem阶段用系统默认颜色又画了一遍。解决办法确认ITEMPREPAINT分支严格返回CDRF_NOTIFYSUBITEMDRAW检查是否把行背景填充逻辑写进了SUBITEMPREPAINT导致重复覆盖。6.2 FlatSB颜色设置后毫无反应滚动条还是系统灰现象调用了FlatSB_SetScrollProp设置track和thumb颜色程序运行起来滚动条没有一丝变化。原因目标exe启用了ComCtl32 v6视觉样式FlatSB的颜色能力在主题接管后直接失效这不是代码写错是机制被剥夺了。解决办法如果你一定要用FlatSB可以在exe的manifest里关掉视觉样式但代价是整个程序都退回经典样式一般不接受。更稳的对策就是用本章开头讲的自绘CScrollBar替换方案彻底绕开主题。6.3 表头排序箭头出现残影点击后不刷新现象点击列头排序后箭头没有立即出现或者箭头还停留在旧列上鼠标在表头上晃一下才恢复正常。原因HDS_OWNERDRAWN开启后系统不再自动重绘画排序箭头而HDN_ITEMCLICK通知又没有触发表头重绘。解决办法在CMyListCtrl里同时处理HDN_ITEMCLICKA和HDN_ITEMCLICKW两个消息并在其中调用m_wndHeader.Invalidate()强制表头立即重画。6.4 子类化表头后退出对话框时崩溃现象程序关闭对话框时在CMyListCtrl析构或清理阶段偶发崩溃调用栈指向CMyHeaderCtrl内部。原因CHeaderCtrl是CListCtrl的子窗口析构顺序可能晚于CMyListCtrl如果子类对象m_wndHeader先被销毁窗口仍然指向一个失效的WndProc。解决办法在CMyListCtrl的OnNcDestroy或接管WM_DESTROY时先调用m_wndHeader.UnsubclassWindow()和m_wndScrollBar.UnsubclassWindow()解除子类化再走默认析构。这个后悔药我在多个项目里都用上了稳定性提升明显。6.5 自绘滚动条thumb比例在DPI缩放下错位现象系统DPI从100%切到150%自绘滚动条thumb变得过短或过长拖动摇杆时位置和内容不匹配。原因自绘滚动条计算thumb高度时用了像素高度但SCROLLINFO里的nPage、nMax是ListView内部逻辑单位在DPI缩放时两者比例失配。解决办法用GetDpiForWindow或GetSystemMetrics拿到当前DPI系数将逻辑单位乘以缩放后再用于thumb高度或者把所有相关尺寸都设为按DPI动态计算不要在构造时写成死值。6.6 开启LVS_EX_DOUBLEBUFFER后自绘行背景闪烁现象加了LVS_EX_DOUBLEBUFFER消除默认闪烁但自绘的items背景在滚动时反而闪得更厉害。原因双缓冲和NM_CUSTOMDRAW的时机产生冲突你的背景绘制可能发生在内存画布上随后又被系统以未更新画布覆盖了部分区域。解决办法改用CMyListCtrl整体双缓冲在OnEraseBkgnd里返回TRUE并把NM_CUSTOMDRAW里的绘制目标从hdc改成一张持久位图或者干脆去掉LVS_EX_DOUBLEBUFFER只保留NM_CUSTOMDRAW的按需绘制通常已经足够顺滑。7. 进阶把三件套的颜色收进一个Theme对象实现一键换肤三件套各自能自绘之后最麻烦的其实是维护一致性。改一处深色滚动条、表头、列表项三处都要跟着改否则界面就会“半浅半深”。我的做法是把所有颜色和尺寸收进一个Theme结构体CMyListCtrl内部分发给三个子对象对话框只需一次调用即可整体换肤。struct ListTheme { COLORREF clrItemText; COLORREF clrItemSelText; COLORREF clrItemBkg; COLORREF clrItemSelBkg; COLORREF clrHeaderText; COLORREF clrHeaderBkg; COLORREF clrHeaderBorder; COLORREF clrScrollTrack; COLORREF clrScrollThumb; COLORREF clrScrollThumbHover; int nHeaderHeight; int nScrollBarWidth; }; void CMyListCtrl::ApplyTheme(const ListTheme theme) { m_theme theme; // items直接作为成员变量供Custom Draw使用 // header分发给表头子类 m_wndHeader.SetColors(theme.clrHeaderText, theme.clrHeaderBkg, theme.clrHeaderBorder); m_wndHeader.SetItemSize(theme.nHeaderHeight); // scrollbar分发给自绘滚动条子类 m_wndScrollBar.SetColors(theme.clrScrollTrack, theme.clrScrollThumb); m_wndScrollBar.SetWidth(theme.nScrollBarWidth); // 全部触发重绘 Invalidate(); m_wndHeader.Invalidate(); m_wndScrollBar.Invalidate(); }这个设计的验证方式很简单切到深色主题后依次检查三个部件的底色是否一致、分隔线是否清晰、滚动条在悬停和按下状态下的反馈是否正常。切换主题后如果发现某个部件颜色没跟上不需要逐行查绘制代码先看Theme对象里对应字段有没有传入这是最快定位手段。回想我之前做深色版列表第一版只改了items文字颜色表头白底还露在外面被产品截图挂在墙上当反面教材。后来把三件套统一收进Theme对象每次只动参数、不碰绘制逻辑后面所有颜色改动都稳定多了。希望你重绘自己的CListCtrl时少走那段别扭路一次把scrollbar、headerctrl、items三件套做到位希望帮到你。本文还有配套的精品资源点击获取
返回列表