MFC CListCtrl重绘实战:滚动条、表头与列表项自绘指南
2026/9/2 4:01:13 网站建设 项目流程

简介:面向MFC界面开发者的CListCtrl重绘资源包,聚焦滚动条、表头和列表项三类元素的个性化定制,适合需要提升列表控件视觉效果、摆脱系统默认样式的Win32/MFC项目。资源共42个文件,其中14个位图用于界面皮肤,9个头文件与7个cpp文件构成核心实现类,还包含工程文件、资源脚本、说明文本等,整体压缩后仅87KB,轻量易用。已有325人学习浏览,适合中高级MFC开发者参考。资源内含SkinList_demo示例工程,以及SkinListCtrl、SkinHeaderCtrl、SkinHorizontalScrollbar等完整类实现,通过继承CScrollBar/CHeaderCtrl并重绘OnPaint,配合WM_HSCROLL/WM_VSCROLL消息处理,完成滚动条外观与交互定制;列表项部分采用OwnerDraw方式,可控制每项的颜色、图标和文字效果。随附说明文档详细解释实现思路与集成方法,可直接嵌入既有项目,快速打造个性化列表界面。 做MFC界面开发这么多年,重绘CListCtrl算是我每次都躲不开的第一道坎。系统默认的列表控件放在那些高度定制化的客户端里,简直就像在西装里面穿了条牛仔裤——违和感拉满。更别提那个“选中行后再点到别的控件,背景色就变成灰扑扑”的经典毛病,不少刚接触自绘的朋友第一个问题就是:怎么让选了却失焦的行保持蓝色?

这篇文章就从实际工程出发,把重绘CListCtrl的滚动条、表头(HeaderCtrl)和列表项(Items)这件事拆开揉碎。不光给出能直接抄的代码,还会说清楚每个选择背后的考虑,以及在MFC这种“半老不新”的框架里做自绘时最容易撞上的几个坑。适合正在改造列表外观、准备做自己的定制控件、或者被“失焦变灰”折腾过的朋友看。

1. 为什么默认CListCtrl外观让人想重绘

1.1 默认风格的几个痛点

系统默认的CListCtrl在WinXP、Win7、Win10和Win11上呈现完全不同的样式。问题是,你的产品界面一旦确定了视觉风格,就不可能跟着操作系统走。默认控件在报告模式下,表头是一层塑料感的渐变渐变,滚动条是最朴素的系统矩形,选中行则是“深蓝色失焦变浅灰”的状态。对于追求统一视觉的客户端程序来说,这三种元素无法与我们自己设计的自定义控件共存。

还有一个很现实的问题,默认CListCtrl的表头不支持设置高度,不支持设置字体颜色,滚动条的宽度完全由系统决定,选中行的背景颜色也不是一个简单属性可以直接改的。更麻烦的是,在“聚焦”和“失焦”之间系统会强制切换颜色,这导致很多人在选中后一点其它按钮,列表里蓝条就变成了灰条,视觉上像是被取消了选中状态,用户会产生误判。

1.2 重绘方案的选型对比

遇到这类问题,很多人第一时间想到的是用“双缓冲”、“绘制到内存DC”之类的技巧,但CListCtrl的自绘核心根本不在这里。真正的核心问题有两个:滚动条属于非客户区,需要子类化处理WM_NCPAINT或者直接使用自绘滚动条库;表头和列表项属于客户区,可以用NM_CUSTOMDRAW通知来接管。

方案上主要有两条路:一是直接子类化CListCtrl,在内部处理所有绘制逻辑,这个做法可控性最高,但代码量也最大;二是使用MFC的反射机制和Virtual CListCtrl扩展,把细节隐藏在基类里。我们这里以“子类化CListCtrl + 处理NM_CUSTOMDRAW”为主,这也是目前社区里最通用、最灵活的路线。

2. 滚动条重绘:先解决最显眼的“违和感”

2.1 滚动条重绘的两种主流路线

很多人以为在CListCtrl里重绘滚动条,只需要处理WM_NCPAINT然后画一个矩形就行。实际上CListCtrl呈现出来的滚动条是一个非常复杂的组合体:包含垂直和水平滚动条、上下箭头按钮、滑块、页面边框,而且它们都在同一个非客户区。直接画一次,并不足够覆盖所有交互状态(按下、禁用、悬停都会触发系统擦除重绘)。

比较常见的两种做法中,一种是“子类化滚动条并取代默认滚动条”,通过SIF_ALL或者是发送WM_NCCALCSIZE控制滚动条的显示尺寸,然后完全用自己创建的CScrollBar子类覆盖上去。另一种是“修改系统滚动条肤色”,在Vista以后系统本身支持CSS的滚动条自定义,但MFC中我们没法直接用CSS,所以只能处理WM_NCPAINT自己画。

实际工程中,我更推荐使用后者“重绘非客户区”的方式,因为代码稳定性更高,不会因为窗口创建顺序或布局变化而错位。具体方法是:在子类化CListCtrl中拦截WM_NCPAINT,然后用GetWindowDC获取窗口DC,用ExcludeClipRect排除客户区后,直接绘制滚动条轨道、滑块和箭头。

2.2 使用自绘滚动条类的实现要点

下面这段代码示意了在CListCtrl子类中,通过处理WM_NCPAINT来完成自定义滚动条绘制的基本思路:

void CSkinListCtrl::OnNcPaint() { CWnd::OnNcPaint(); // 先让系统绘制非客户区(包括边框) // 获取窗口DC后,我们需要手动绘制滚动条区域 CWindowDC dc(this); CRect rcWnd; GetWindowRect(&rcWnd); // 计算垂直滚动条区域:窗口右侧一个固定宽度 CRect rcVScroll = rcWnd; rcVScroll.left = rcVScroll.right - m_nScrollWidth; // 因为坐标系是屏幕,需要转换回来 ScreenToClient(&rcVScroll); // 这里用自定义颜色绘制轨道 dc.FillSolidRect(rcVScroll, RGB(240,240,240)); // 滑块位置需要根据当前滚动条位置来计算 SCROLLINFO si = { sizeof(SCROLLINFO) }; GetScrollInfo(SB_VERT, &si); int nThumbHeight = /* 根据页大小和范围计算滑块高度 */; CRect rcThumb = /* 根据si.nPos计算滑块矩形 */; dc.FillSolidRect(rcThumb, RGB(180,180,180)); }

上述代码只是一个框架,真实应用中还需要处理几个重要细节。第一,你必须把滚动条的“系统绘制”彻底禁掉,否则系统会在WM_NCPAINT之前或之后重画一遍我们的自定义内容,造成闪烁。通常的做法是覆写WM_NCCALCSIZE并返回特定值,或者在WM_NCPAINT函数里先将整个非客户区绘制为背景色再绘制滚动条。第二,滑块高度计算公式建议为:

int nPage = si.nPage; int nMax = si.nMax; // 注意,nMax包含nPage int nRange = nMax - nPage; nThumbHeight = (nPage * rcTrackHeight) / (nMax + 1); nThumbPos = (si.nPos * (rcTrackHeight - nThumbHeight)) / (nRange > 0 ? nRange : 1);

不然滑块会变得不合比例,拖动时会有严重的错位感。

2.3 为何不推荐直接用“经典滚动条样式”

有些朋友偷懒,直接给窗口加上一句:

SetWindowLongPtr(this->m_hWnd, GWL_STYLE, GetWindowLongPtr(this->m_hWnd, GWL_STYLE) | WS_BORDER | WS_VSCROLL);

或者简单地通过ModifyStyle带上“WS_VSCROLL | WS_HSCROLL”,但这样只能保证滚动条出现,无法控制它的粗细、颜色和交互反馈。如果只想要“细滚动条”,甚至在Win11上系统会自动加上圆角悬浮样式,那就不能和在Win7上保持一致。这也是我坚持做WM_NCPAINT自绘的根本原因——不同操作系统下界面像素级一致是客户端软件的底线。

3. 表头重绘:从响应通知到自定义绘制

3.1 拦截HDN通知还是NM_CUSTOMDRAW

CListCtrl在报告模式下自带一个Header Control,可以通过GetHeaderCtrl()拿到子窗口句柄。要重绘表头,有两种主流姿势:一种是在父窗口(CListCtrl所在对话框)中处理HDN_ITEMDRAW通知;另一种是得到CHeaderCtrl的CWnd指针后子类化它,在子类里处理WM_PAINT或者NM_CUSTOMDRAW。

比较两路,HDN_ITEMDRAW的优势是能拿到每个列的Item信息,而且不用自己维护窗口消息,但项目多时重绘控制稍微麻烦一些。NM_CUSTOMDRAW则更直接,它允许我们在表头绘制过程中针对每个item返回CDRF_NEWFONT或CDRF_SKIPDEFAULT,实现完全自绘,代码结构也更紧凑。

我推荐的做法是子类化CHeaderCtrl,然后在NM_CUSTOMDRAW处理函数中自绘表头背景、文字和边框。这样做的好处是,在CListCtrl子类内部处理所有绘制,外部调用方不需要关心表头内部逻辑,且方便内存DC提前绘制,防止刷新时闪烁。

3.2 实现自绘表头:绘制背景与热跟踪效果

下面是一个典型的重绘表头代码段:

void CSkinHeaderCtrl::OnNMCustomDraw(NMHDR *pNMHDR, LRESULT *pResult) { NMCUSTOMDRAW* pNMCustomDraw = (NMCUSTOMDRAW*)pNMHDR; *pResult = CDRF_DODEFAULT; if (pNMCustomDraw->dwDrawStage == CDDS_PREPAINT) { *pResult = CDRF_NOTIFYITEMDRAW; return; } if (pNMCustomDraw->dwDrawStage == CDDS_ITEMPREPAINT) { // 获取当前所绘制的表头柱索引 int nIndex = (int)pNMCustomDraw->dwItemSpec; CRect rcItem = pNMCustomDraw->rc; CDC* pDC = CDC::FromHandle(pNMCustomDraw->hdc); if (!pDC) return; // 背景 pDC->FillSolidRect(rcItem, RGB(250,250,250)); // 模拟分割线 pDC->Draw3dRect(rcItem, RGB(200,200,200), RGB(120,120,120)); // 绘制文字 TCHAR szText[256] = {0}; HDITEM hd = {0}; hd.mask = HDI_TEXT | HDI_FORMAT; hd.pszText = szText; hd.cchTextMax = _countof(szText) - 1; GetItem(nIndex, &hd); CRect rcText = rcItem; rcText.left += 6; pDC->SetTextColor(RGB(80,80,80)); pDC->SetBkMode(TRANSPARENT); pDC->DrawText(szText, rcText, DT_LEFT | DT_VCENTER | DT_SINGLELINE); // 如果是当前按下或悬停状态,绘制一个高亮背景 if (nIndex == m_nHoverIndex) { pDC->FillSolidRect(rcItem, RGB(230,245,255)); } *pResult = CDRF_SKIPDEFAULT; // 让系统不再绘制默认内容 } }

在实现过程中,我踩过一个坑:Header控件的文字默认会有一个系统设置的偏移,如果直接这么绘制,文字会跟排序箭头以及列间距冲突。需要额外检查HDI_FORMAT中是否有HDF_SORTUP或HDF_SORTDOWN标志,并在绘制文字之前把箭头空间单独预留出来。否则,重绘之后所有排序列的箭头不是莫名其妙消失,就是文字被箭头遮挡。

3.3 如何让表头高度可自定义

表头高度的自定义也是重绘的诉求之一。CListCtrl默认表头高度由系统根据字体决定,并不提供公共的属性接口。重绘表头后,可以通过发送HDM_SETITEM消息来实际改变表头高度,或者在子类化后的CHeaderCtrl中重写Layout函数。常用做法是重写CHeaderCtrl的OnLayout:

void CSkinHeaderCtrl::OnLayout(HDHDR *phdhdr, LRESULT *pResult) { // CHeaderCtrl的重置布局时机 DefWindowProc(HDM_LAYOUT, 0, (LPARAM)phdhdr); // 修改每项的高度为你期望的像素值 if (phdhdr->rgrc[0].bottom - phdhdr->rgrc[0].top > m_nHeaderHeight) { phdhdr->rgrc[0].bottom = phdr->rgrc[0].top + m_nHeaderHeight; } *pResult = TRUE; }

这样做的好处是,CListCtrl整体窗口布局也会随之调整,表头和子项显示区域的分界不再僵硬。

4. 列表项重绘:状态处理是灵魂

4.1 Owner-Draw与NM_CUSTOMDRAW我怎么选

CListCtrl中“Items”的重绘有两个层面:一种是整行(每行作为一个自绘单元),另一种是每个子项(每个单元格)独立绘制。对于“选中行蓝色、失焦变灰”这种状态处理,最好的方式是使用NM_CUSTOMDRAW通知,因为它提供了更细粒度的绘制阶段控制。

我建议在CListCtrl子类里面响应NM_CUSTOMDRAW。下面是核心处理逻辑框架:

void CSkinListCtrl::OnNMCustomdraw(NMHDR *pNMHDR, LRESULT *pResult) { NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>(pNMHDR); *pResult = CDRF_DODEFAULT; if (CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage) { *pResult = CDRF_NOTIFYITEMDRAW; return; } if (CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage) { *pResult = CDRF_NOTIFYSUBITEMDRAW; return; } if ((CDDS_SUBITEM | CDDS_ITEMPREPAINT) == pLVCD->nmcd.dwDrawStage) { // 拿到当前行和列 int nRow = static_cast<int>(pLVCD->nmcd.dwItemSpec); int nCol = pLVCD->iSubItem; CDC* pDC = CDC::FromHandle(pLVCD->nmcd.hdc); if (!pDC) return; // 判断行状态 BOOL bSelected = (GetItemState(nRow, LVIS_SELECTED) & LVIS_SELECTED) != 0; BOOL bFocused = (GetFocus() == this); CRect rcSub; GetSubItemRect(nRow, nCol, LVIR_BOUNDS, rcSub); // 背景色:失焦时仍然用我们自己的高亮色,而不是系统灰 COLORREF clrBk = RGB(255,255,255); if (bSelected) { clrBk = bFocused ? RGB(51,153,255) : RGB(51,153,255); // 都保持同一种蓝 } pDC->FillSolidRect(rcSub, clrBk); // 文字颜色 pDC->SetTextColor(bSelected ? RGB(255,255,255) : RGB(51,51,51)); // 绘制文本 CString strText = GetItemText(nRow, nCol); pDC->SetBkMode(TRANSPARENT); pDC->DrawText(strText, rcSub + CRect(4,0,0,0), DT_LEFT | DT_VCENTER | DT_SINGLELINE); *pResult = CDRF_SKIPDEFAULT; } }

代码中,我特意在“bFocused ? …”两处都用了同一种蓝色RGB(51,153,255),这样不管列表控件有没有焦点,选中行都会保持蓝色,从根本上解决“失焦变灰”问题。

4.2 绘制不同状态:悬停、选中、禁用

除了选中和失焦状态之外,列表项自绘还应该关注鼠标悬停状态。用户界面里,悬停反馈在光标移动到行上时应该出现浅色背景,离开时恢复。这个可以通过检测鼠标位置与行矩形是否相交来实现,不需要额外处理消息。

我通常会在NM_CUSTOMDRAW中根据当前鼠标位置来点亮单行悬停:

CPoint ptCursor; GetCursorPos(&ptCursor); ScreenToClient(&ptCursor); for (int i = 0; i < GetItemCount(); i++) { CRect rcItem; GetItemRect(i, &rcItem, LVIR_BOUNDS); if (rcItem.PtInRect(ptCursor)) { if (m_nHoverRow != i) { m_nHoverRow = i; Invalidate(); } break; } }

但这种方法需要小心,防止在每次移动时刷新整个列表造成闪烁。更好的办法是保存上一行悬停状态,只有悬停行变化时才局部刷新那一行和旧行的区域。

4.3 取消所有选择行的实现

有朋友会问“clistctrl取消所有选择行”,其实这句一般在两个场景中出现:一个是业务逻辑上需要“不得有任何行处于选中状态”,另一个是程序初始化时清除上一次残留的选中状态。最简单的实现方式是:

SetItemState(-1, 0, LVIS_SELECTED);

注意这里第三个参数的掩码如果只传LVIS_SELECTED,就会只清除选中状态而保留其它状态。如果还想让焦点框同时消失,可以再加一个:

SetItemState(-1, 0, LVIS_SELECTED | LVIS_FOCUSED);

这种清空操作在重绘中并不会造成额外问题,因为自绘时读取到的bSelected自然为FALSE。某些场景下,希望在点击空白区域时清空选中行,则需要在子类中处理WM_LBUTTONDOWN,检查点击位置是否超出了所有行的范围后执行上面的清除代码。

5. 失焦不变灰的实战解法

5.1 问题根源:系统默认的“失焦颜色”

在CListCtrl中,一旦控件失去焦点,系统就会用GetSysColor(COLOR_BTNFACE)或者系统主题中的“inactive selection”颜色来替换选中行背景。很多自绘尝试无效,根本原因是没有真正接管列表项绘制,或者只是在自定义绘制中修改了文本颜色但仍把背景绘制交给了系统。系统会以默认矩形背景覆盖颜色,导致选择状态的颜色控制失败。

采用NM_CUSTOMDRAW并返回CDRF_SKIPDEFAULT之后,我们就完全接管了背景和文字的绘制。这样一来,系统的“失焦变灰”逻辑就被绕开了。代码中一个比较容易被忽略也容易出问题的点在于“判断是否聚焦”时,要考虑到CListCtrl虽然不是一个窗口焦点,但它的父窗口可能依然有焦点,比如在子对话框中,列表框本身不接收焦点,你可能希望它始终处于选中高亮状态,因此最好把判断条件放宽:

BOOL bListHasFocus = (GetFocus() == this || GetFocus()->GetParent() == this);

如果GetFocus()返回的是我们嵌入的编辑框或下拉框,也要视为有焦点。

5.2 自定义绘制中固定高亮色的方案

另一种更简单粗暴的方案是,不为“有无聚焦”设定两套颜色,而是始终用一套颜色绘制选中行。在很多仪表盘、监控类客户端的列表里,选中行在全局视觉上需要一直保持醒目。所以在我的CSkinListCtrl里,直接定义了一个成员变量m_clrSelection,默认情况下是蓝绿色,并不会随焦点丢失而改变。

void CSkinListCtrl::SetSelectionColor(COLORREF clr) { m_clrSelection = clr; Invalidate(FALSE); }

然后在绘制时只用这个颜色,不再依赖焦点判断。这种做法虽然牺牲了“失焦后降低高亮”的系统视觉习惯,但换来了视觉一致性和用户认知的稳定。我们在项目中实测,用户反馈很好,不再因为焦点切换而误以为“还没选中”。

5.3 结合热词“clistctrl选中的后蓝色”的注意事项

热搜词里还有“mfc clistctrl 选中的后蓝色”,这个其实是很多用户想知道“如何让选中后显示蓝色背景”的问题。如果在不重绘的情况下,只需要把CListCtrl的风格加上LVS_SHOWSELALWAYS,就可以让失去焦点时依然显示选中项高亮,但颜色仍然是系统指定的灰蓝。如果想要纯正的蓝色,只能采用自绘。所以这两种方法的取舍要视需求而定:

  • 不想自绘,只希望“失焦不变灰”:使用LVS_SHOWSELALWAYS样式,简单但颜色偏灰,不同系统效果不完全一致。
  • 希望颜色完全可控:使用NM_CUSTOMDRAW自绘,这一点在上面已经给出了代码路径。

我自己的项目里,通常两种会结合使用:先加LVS_SHOWSELALWAYS,保证在未被自绘覆盖的区域也不会出现“灰到看不见”的尴尬,然后通过自绘把选中背景统一成自己定义的高亮蓝。

6. 性能与稳定性:重绘过程中踩过的坑

6.1 避免在CustomDraw中做重量级操作

NM_CUSTOMDRAW会在鼠标移动、滚动、焦点切换时被大量触发。如果在这个处理函数里去GetItemText拼接字符串、创建新的CFont、加载位图,很容易导致列表滚动时CPU占用飙升、界面卡顿。

我的经验是:把常用的字体、画刷、位图预先创建为成员变量或缓存表,CustomDraw中只使用它们,不做任何创建或释放操作。比如:

// 在OnInit或构造函数中创建画刷 m_brSelection.CreateSolidBrush(RGB(51,153,255)); m_brNormal.CreateSolidBrush(RGB(255,255,255)); m_fontText.CreateFont(-12, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY, FIXED_PITCH, _T("Microsoft YaHei"));

在绘制时直接选入,绘制完毕后再选回原字体,避免每行重复创建。

6.2 双击/拖动时重绘闪屏

自绘列表项后,闪屏通常来自“绘制背景+绘制文字分开两步”。例如先FillSolidRect填充背景,再用DrawText画文字,如果列表项尺寸较大并且刷新频率高,就会出现白色闪烁。

解决办法是使用内存DC。把整个“行”的绘制先画到内存位图中,再一次BitBlt到目标DC。对列表项来说,如果仅仅绘制一行,一次内存绘制开销并不大,但在多列的情况下要注意一次性把当前子项及其背景绘制完整,减少多次Invalidate。

我习惯在绘制分组时为一整行建立一个内存DC,然后针对每个子项切换绘制区域,这样整行刷新时闪烁很小。

6.3 与CListCtrl固有行为冲突的坑

最后说说几个与系统行为冲突的坑。自绘后,你会发现自己手动绘制的排序箭头和系统排序状态不同步,因为CHeaderCtrl的重绘不会感知到HDN_ITEMCLICK消息。必须自己处理HDN_ITEMCLICK并更新绘制状态。其次,CListCtrl自绘后,默认的grid line(网格线)也会消失,如果还需要网格线效果,建议在NM_CUSTOMDRAW中自己画每行的分隔线。

还有更隐蔽的一个问题:自绘后选中行的背景覆盖了整行,但如果你按下键盘上下键,焦点框(focus rect)还是会被系统绘制出来。此时你应该在CDDS_PREPAINT阶段返回CDRF_NOTIFYITEMDRAW,然后在绘制环节把焦点虚线框取消掉,或者调用HideCaret类似的方式处理。否则,会出现“自绘的高亮和系统虚线框重叠”的丑陋效果。解决方式是在绘制末尾调用:

pDC->DrawFocusRect(rcSub);

或者干脆不调用,让焦点框不再显示。

6.4 实测建议:滚动条和列表项联动

自绘滚动条和列表项之间其实存在联动。当滚动条被拖动时,列表客户区会不断重绘,而WM_NCPAINT如果不会同步触发,滚动条的滑块位置就会略微滞后于内容。这属于正常的Windows刷新机制,但如果你在自绘滚动条时依赖GetScrollInfo(si.nPos)来定位滑块,你会发现它直到拖动结束才会被更新。

正确的做法是,在CListCtrl子类中响应WM_VSCROLL、WM_HSCROLL、WM_MOUSEWHEEL之后主动调用:

SetScrollPos(SB_VERT, si.nPos); Invalidate(FALSE); SendMessage(WM_NCPAINT, 0, 0);

其中SendMessage(WM_NCPAINT)会强制非客户区重绘,让滚动条滑块与列表内容同步,这也是我在自绘过程中最常被人忽略的一个细节。

写在最后:一点个人经验

重绘CListCtrl并不是一项可以依赖“抄一段代码就完事”的工作,它需要你把许多细节装进脑子里:滚动条的非客户区绘制、表头的热跟踪、列表项的焦点判断,每一样都藏着“系统默认行为”的阴影。真正稳定高效的代码,是在不断适配不同系统、多种字体和遇到各种用户操作习惯后逐步打磨出来的。

如果你现在正准备改造一个列表控件,我的建议是从“列表项自绘”开始,先解决视觉上最明显的选中状态问题,再逐步处理表头和滚动条。每完成一层,就实际运行一段时间观察滚动、拖选、键盘操作是否正常。等你把这三层都接管了,你得到的就不仅仅是一个“不变灰的列表”,而是一个完全由你掌控的UI组件。

另外,关于“取消所有选择行”和“失去焦点不变灰”这两个高频问题,其实绝大多数情况下都可以通过一行样式或者一个自绘分支解决,并不需要把整个控件重写。看清问题本质再下手,比盲目套用别人的封装类要可靠得多。这些经验,希望能帮你少踩几个坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询