MFC自绘柱状图控件:从GDI绘图到双缓冲完整实践指南
简介面向MFC初学者的柱状图绘制示例工程演示在单文档视图中通过画刷与图形设备接口GDI绘图从TXT文本文件读取数据并渲染柱状图。压缩包共36个文件大小约2.28MB包含头文件、C源码、MFC工程配置、说明文档及可直接运行的exe程序便于对照学习MFC应用框架、CBrush画刷、CPaintDC设备上下文与CFile文件读取等核心知识点。已有599人学习浏览。该工程完整覆盖从文件数据解析到矩形柱体填充的实现链路包含OnPaint消息响应、SelectObject选入画刷、Rectangle绘制柱体、GetClientRect计算绘图区域等关键细节代码结构清晰、目录紧凑适合初学者在现有基础上扩展坐标轴、标题、图例及动态刷新功能。配套描述文本与数据样例可帮助验证文件读取流程通过调试运行可直观看到数据变化与柱状图更新的对应关系适合课堂练习或课程设计二次开发。1. 把简单柱状图画进 MFC 窗口到底需要什么MFC 里画柱状图说起来不过几行 GDI 绘图代码但真正动手时坐标原点、GDI 对象生命周期、双缓冲这些老朋友会一个个冒出来把新手按在地上摩擦。这个项目的核心诉求是不引第三方库、不装图表控件用一个自绘控件在当前窗口里把一批数据显示成柱子并且能控得住样式和数据更新。适合已经写过 MFC 对话框基础程序想自己掌控绘图逻辑、又不想引入重型图表库的开发者。我见过不少团队为了一个柱状图引入几百兆的图表库最后内网部署环境装不上、授权又卡壳白白折腾一周。自己画反而干净一个 CStatic 子类、一两个回调函数、几个 GDI 对象就能交付一个够用的柱状图控件。这篇文章会从最小可运行代码讲起一直推到数据提示、命中检测、双缓冲和负数坐标处理全程按可复现的方式写步骤里每段代码都标了参数含义和翻车点。2. 从零搭建自绘柱状图控件核心类和第一次跑通2.1 为什么要从 CStatic 派生而不是直接在对话框上画我一般先建一个 CStatic 的派生类来做柱状图控件。原因很直接CStatic 天然是窗口可以接收鼠标消息便于后续做点击选中、数据提示它又能作为子控件放进对话框不用改主窗口的绘图流程一个控件管好自己的重绘。这个模式在 MFC 自绘控件里最常用也是后面所有扩展的基础。CStatic 派生的第一步是重写 OnPaint。OnPaint 和 OnDraw 不一样OnDraw 要配合 CView 的文档视框架构而 OnPaint 是任何窗口都有的消息响应。作为控件重写 OnPaint 最直观因为每次窗口需要刷新的时候系统都会把 WM_PAINT 送过来直接在 OnPaint 里完成全部绘图即可。文件头里要声明消息映射这是 MFC 的老规矩。// BarChartCtrl.h #pragma once class CBarChartCtrl : public CStatic { public: CBarChartCtrl(); virtual ~CBarChartCtrl(); // 外部传入数据 void SetData(const std::vectordouble values); protected: afx_msg void OnPaint(); DECLARE_MESSAGE_MAP() private: std::vectordouble m_data; };逻辑说明SetData 把外部数据拷贝进控件内部然后调用 Invalidate 触发重绘。数据拷贝而不是持有引用是为了防止外部 vector 生命周期结束导致悬空指针这是控件类必须做的隔离。消息映射 DECLARE_MESSAGE_MAP 宏是 MFC 消息路由的入口缺少它 OnPaint 不会被调用。参数说明m_data 存的是 double 类型因为实际业务里柱状图经常要显示百分比、温度、耗时这类带小数的数据用 double 才能保证后续坐标换算的精度。如果确认业务数据全是整数可以换回 int但 double 的兼容性更好建议保留。2.2 创建控件的最小命令从对话框初始化到窗口显示控件类写完下一步是在对话框的 OnInitDialog 里创建实例。这一步最简单的做法是用 Create 函数生成一个子窗口指定坐标和控件 ID。// DialogInit 中的创建代码 CBarChartCtrl* m_pChart new CBarChartCtrl; m_pChart-Create(_T(STATIC), WS_CHILD | WS_VISIBLE, CRect(20, 20, 420, 260), this, 1001); m_pChart-SetData({ 34.5, 67.8, 23.9, 88.2, 45.6 });逻辑说明Create 的第一个参数传 STATIC 是沿用了 CStatic 的窗口类名这样窗口在系统里注册为静态控件类型但视觉样式完全由我们自己绘制不受系统默认样式干扰。WS_CHILD 表示这是一个子窗口WS_VISIBLE 让它创建后立即可见。CRect 指定了控件在对话框内的位置和尺寸。参数说明1003 是控件 ID后面加 ToolTip 或做消息路由时要用到它。这里 new 出来的对象在对话框销毁时要记得 delete建议在对话框的 OnDestroy 里配合 delete 处理否则每次关闭对话框都会泄漏一块堆内存。后来我习惯在对话框类里把 CBarChartCtrl 设成成员变量而不是指针省去手动管理内存的麻烦代码更安全。2.3 绘制柱状图的核心逻辑坐标换算和第一个版本OnPaint 里做三件事取客户区矩形、计算绘图区边距、循环绘制每一根柱子。MFC 的坐标原点在窗口左上角X 轴向右Y 轴向下这意味着值越大对应的屏幕 Y 坐标越小柱子要画在底部向上长所以画柱子的矩形时必须用「绘图区底部减去柱高」作为矩形上边界的 Y 坐标。void CBarChartCtrl::OnPaint() { CPaintDC dc(this); CRect rcClient; GetClientRect(rcClient); // 四边留出空白给坐标轴和标签腾位置 const int nLeftMargin 50; const int nRightMargin 20; const int nTopMargin 10; const int nBottomMargin 30; CRect rcPlot(nLeftMargin, nTopMargin, rcClient.right - nRightMargin, rcClient.bottom - nBottomMargin); // 找到数据最大值作为纵向归一化基准 double dMaxVal 0; for (double v : m_data) { if (v dMaxVal) dMaxVal v; } if (dMaxVal 0) dMaxVal 1; // 横向上每根柱子占的像素宽度以及柱体宽度 int nCount (int)m_data.size(); int nStep rcPlot.Width() / nCount; int nBarWidth (int)(nStep * 0.7); // 绘制每一根柱子 for (int i 0; i nCount; i) { int nBarHeight (int)(rcPlot.Height() * m_data[i] / dMaxVal); int nLeft rcPlot.left i * nStep (nStep - nBarWidth) / 2; CRect rcBar(nLeft, rcPlot.bottom - nBarHeight, nLeft nBarWidth, rcPlot.bottom); CBrush brush(RGB(64, 128, 192)); dc.FillRect(rcBar, brush); // 给柱子加一个深色边框视觉上更立体 dc.Draw3dRect(rcBar, RGB(200, 220, 240), RGB(40, 80, 120)); } // 画基线和左侧纵轴 dc.MoveTo(rcPlot.left, rcPlot.bottom); dc.LineTo(rcPlot.right, rcPlot.bottom); dc.MoveTo(rcPlot.left, rcPlot.top); dc.LineTo(rcPlot.left, rcPlot.bottom); }逻辑说明nBarHeight 是换算的核心它把业务数值映射到绘图区高度内的像素值。rcPlot.bottom - nBarHeight 这个表达式就是前面说的 Y 轴方向反转不写这一行柱子会倒着长这是新手最容易翻车的地方。nStep 是每个柱子分配的横向区间nBarWidth 取区间宽度的 70%剩下的 30% 自然形成柱子间距无需额外计算间距。参数说明四边留白值是按实际需求调的左边 50 像素是为了给 Y 轴刻度文字留空间底部 30 像素是给 X 轴标签留位置。如果你不需要横轴标签nBottomMargin 可以压到 10。RGB(64, 128, 192) 是柱子主色想换颜色直接改这里。FillRect 和 Draw3dRect 组合使用是先填充后描边的常见做法。这段代码跑通后你已经有一个能显示柱子的控件了。但离「能交付」还差得远Y 轴没有刻度、数据大了柱子会顶出绘图区、窗口拉伸柱子挤成一团。接下来的内容就是把这块补全。3. 柱状图的美观化与参数化刻度、标签和渐变效果3.1 轴刻度和数值标签怎么画字体、对齐和格式化上一章的控件只画了干巴巴的柱子没有刻度就没有参考意义。轴刻度的绘制思路是在 Y 轴上等间距取若干个刻度位置计算每个位置对应的业务数值再用 DrawText 或 TextOut 绘制。难点在于 Y 轴数值是倒着的——屏幕 Y 坐标越大值越小这个对应关系搞反会让刻度值跟柱高对不上。void CBarChartCtrl::DrawAxes(CDC* pDC, const CRect rcPlot, double dMaxVal) { // 预留 4 条刻度线包含 0 和最大值 const int nTickCount 5; CFont font; font.CreatePointFont(80, _T(Microsoft YaHei)); CFont* pOldFont pDC-SelectObject(font); pDC-SetBkMode(TRANSPARENT); for (int i 0; i nTickCount; i) { int nY rcPlot.bottom - rcPlot.Height() * i / (nTickCount - 1); double dVal dMaxVal * i / (nTickCount - 1); pDC-MoveTo(rcPlot.left - 4, nY); pDC-LineTo(rcPlot.left 4, nY); CString strText; strText.Format(_T(%.0f), dVal); // 文字右对齐到轴线左侧 CRect rcText(0, nY - 10, rcPlot.left - 8, nY 10); pDC-DrawText(strText, rcText, DT_RIGHT | DT_VCENTER | DT_SINGLELINE); } pDC-SelectObject(pOldFont); }逻辑说明nTickCount 是刻度数量这里定成 5 条从 0 到最大值均分。循环里的 nY 是从底部往上算的i 0 时 nY 是底部i 4 时是顶部正好对应从 0 到最大值的刻度排列。DrawText 使用 DT_RIGHT | DT_VCENTER让刻度文字右对齐并垂直居中于刻度线这样不会歪。参数说明CreatePointFont(80, ...) 里的 80 是字号单位是 1/10 磅8 磅是适合刻度标签的小字号。Microsoft YaHei 比默认的 System 字体在 Win10 以上系统里显示更平滑如果你的部署环境是 XP 或 Win7请换成 SimSun 或 Tahoma。SetBkMode(TRANSPARENT) 非常关键不设置的话文字背景会盖住前面的绘图内容出现白边。3.2 柱子渐变填充一个循环解决立体感问题单色填充的柱状图看起来扁平稍微加一点渐变就能让视觉质量上一个台阶。渐变的核心思想是把柱体从底到顶切成若干个水平细条每条用不同颜色的画刷填充。底部颜色亮、顶部颜色深就能模拟光线从下往上照的效果。void CBarChartCtrl::DrawGradientBar(CDC* pDC, const CRect rcBar) { const int nSteps 20; // 渐变步数 for (int s 0; s nSteps; s) { // 当前细条的上边界和下边界 int nY1 rcBar.bottom - (s 1) * rcBar.Height() / nSteps; int nY2 rcBar.bottom - s * rcBar.Height() / nSteps; // 颜色从底部亮色渐变到顶部暗色 int nR 90 (int)(70.0 * s / nSteps); int nG 150 (int)(50.0 * s / nSteps); int nB 200 - (int)(60.0 * s / nSteps); CBrush brush(RGB(nR, nG, nB)); CRect rcStrip(rcBar.left, nY1, rcBar.right, nY2); pDC-FillRect(rcStrip, brush); } }逻辑说明nSteps 决定渐变的细腻程度20 步在柱宽 30 像素时每一细条约 1.5 像素效果平滑且性能开销很低。颜色变化是线性插值底部 RGB(90, 150, 200)顶部 RGB(160, 200, 140)视觉上从蓝青渐变到黄绿。你不喜欢这个配色直接改 nR、nG、nB 三条式子里的起始值和系数即可。参数说明细条绘制用 FillRect 而不是 PatBlt因为 PatBlt 需要额外指定光栅操作码FillRect 更直观也更不容易踩坑。柱宽太窄时nSteps 要相应调小否则每条细条宽度小于 1 像素会变成纯色填充渐变效果丢失。柱宽超过 60 像素时建议把 nSteps 提高到 40过渡更细腻。3.3 网格线和横轴标签让图表有上下文网格线的作用是帮助读者纵向对比柱高。绘制方法是在画刻度的同时从每个刻度位置向右画一条浅色虚线或细实线。横轴标签则是给每根柱子一个名字通常画在柱子的正下方。这里有一个常见问题文字长度有限如果柱子数量超过 10 根标签就会互相重叠所以一般只显示关键标签或压缩旋转角度。// 网格线和横轴标签 void CBarChartCtrl::DrawGridAndLabels(CDC* pDC, const CRect rcPlot, const std::vectorCString labels) { // 浅色网格线 CPen penGrid(PS_DOT, 1, RGB(200, 200, 200)); CPen* pOldPen pDC-SelectObject(penGrid); for (int i 0; i 4; i) { int nY rcPlot.bottom - rcPlot.Height() * i / 4; pDC-MoveTo(rcPlot.left, nY); pDC-LineTo(rcPlot.right, nY); } pDC-SelectObject(pOldPen); // 横轴标签 int nCount (int)labels.size(); int nStep rcPlot.Width() / nCount; CFont font; font.CreatePointFont(80, _T(Microsoft YaHei)); CFont* pOldFont pDC-SelectObject(font); for (int i 0; i nCount; i) { CRect rcLabel(rcPlot.left i * nStep, rcPlot.bottom 2, rcPlot.left (i 1) * nStep, rcPlot.bottom 22); pDC-DrawText(labels[i], rcLabel, DT_CENTER | DT_VCENTER | DT_SINGLELINE); } pDC-SelectObject(pOldFont); }逻辑说明网格线用 PS_DOT 虚线样式颜色浅灰避免抢占柱子的视觉焦点。横轴标签的矩形宽度等于每个柱子的分配区间 nStep文字居中绘制这样标签会正好落在柱子正下方。DT_SINGLELINE 保证文字不换行超长标签会被截断这是有意为之防止布局错乱。参数说明nCount 超过 10 时中文标签几乎肯定重叠一个变通做法是每隔一根柱子显示一个标签另一个做法是把文字旋转 45 度。我一般优先选隔根显示因为旋转后的可读性并不好。这个方案的代价是丢失部分标签信息但柱状图本质是看趋势不是读精确名字。4. 加一点交互鼠标数据提示和选中高亮4.1 数据提示 ToolTip 的正确接入方式一个不带任何交互的柱状图数据全靠肉眼对照刻度使用体验很差。最常见的增强是鼠标悬停在柱子上时弹出一个 ToolTip 显示该柱子的具体数值。MFC 里给子窗口加 ToolTip 的常规做法是在 PreTranslateMessage 里拦截鼠标移动消息并把消息转发给 CToolTipCtrl。BOOL CBarChartCtrl::PreTranslateMessage(MSG* pMsg) { if (m_toolTip.GetSafeHwnd() ! nullptr) { m_toolTip.RelayEvent(pMsg); } return CStatic::PreTranslateMessage(pMsg); }逻辑说明RelayEvent 是 ToolTip 能跟随鼠标移动的关键。很多人在对话框里创建完 ToolTip 后发现提示永远不出现或者位置不对十有八九是忘了在 PreTranslateMessage 里做这个消息转发。ToolTip 控件本身不捕获鼠标它需要父窗口把消息传递给它。创建 ToolTip 并关联柱状图控件的代码放在 OnInitDialog 里// 创建 ToolTip 并关联控件 m_toolTip.Create(this); m_toolTip.AddTool(m_pChart, _T(柱状图区域)); m_toolTip.Activate(TRUE);参数说明AddTool 的第二个参数是默认提示文本鼠标悬停在控件任意位置时先显示这个文本。要做到每根柱子显示不同数值不能在 AddTool 里写死而要在鼠标移动时动态更新。后面 4.2 里的命中检测正好提供更新时机。4.2 命中检测算法判断鼠标落在哪根柱子上要让 ToolTip 显示具体柱子的值先得知道鼠标当前落在哪个柱子上。命中检测的思路不复杂把每根柱子的矩形区域保存下来当鼠标移动时遍历这些矩形调用 PtInRect 判断鼠标点是否落在其中。保存矩形的容器在每次重绘时更新保证柱子的屏幕位置始终和数据一致。void CBarChartCtrl::OnMouseMove(UINT nFlags, CPoint point) { for (size_t i 0; i m_vecBarRects.size(); i) { if (m_vecBarRects[i].PtInRect(point)) { // 更新提示文本为具体数值 CString strTip; strTip.Format(_T(第 %d 项%.2f), (int)i 1, m_data[i]); m_toolTip.UpdateTipText(strTip, this, 1001); // 选中的柱子索引触发重绘来显示高亮 m_nSelectedIndex (int)i; Invalidate(FALSE); break; } } CStatic::OnMouseMove(nFlags, point); }逻辑说明m_vecBarRects 在 OnPaint 里填充——画柱子时顺便把矩形 push 进这个容器代码路径是同一套坐标计算所以命中检测和绘图的几何一定一致。这是一个容易被忽略的工程细节如果命中检测重新写一遍坐标计算一旦两处代码不一致就会出现「看着柱子在这里鼠标点上去却没反应」的玄学问题。参数说明UpdateTipText 的第三个参数 1001 必须是创建控件时用的那个 ID不匹配就更新失败。m_nSelectedIndex 默认设为 -1表示没有柱子被选中。高亮效果由 OnPaint 根据这个索引绘制一个突出边框加上 Invalidate 触发重绘就形成了实时反馈。4.3 选中柱子的高亮绘制和光标反馈高亮的实现方式是在 OnPaint 画完所有柱子后如果 m_nSelectedIndex 有效就在对应柱子的边界外面再画一个醒目的红色边框并配合光标变化给用户明确的点击反馈。// OnPaint 末尾追加高亮绘制 if (m_nSelectedIndex 0 m_nSelectedIndex (int)m_vecBarRects.size()) { CRect rcSel m_vecBarRects[m_nSelectedIndex]; rcSel.InflateRect(2, 2, 2, 2); CPen penHighlight(PS_SOLID, 2, RGB(220, 40, 40)); CPen* pOldPen pDC-SelectObject(penHighlight); CBrush* pOldBrush (CBrush*)pDC-SelectStockObject(NULL_BRUSH); pDC-Rectangle(rcSel); pDC-SelectObject(pOldPen); pDC-SelectObject(pOldBrush ); }逻辑说明InflateRect 把矩形向外扩大 2 像素让红色边框和柱子本身留出空隙视觉上更清晰。SelectStockObject(NULL_BRUSH) 表示矩形只描边不填充这样不会盖住柱子的渐变效果。光标反馈在 OnMouseMove 里用 SetCursor 完成实心箭头改成向上箭头提示用户这里可以点击。参数说明高亮边框宽度 2 像素颜色 RGB(220, 40, 40)属于警示红在蓝青色柱子上非常醒目。如果想强调选中态而不是悬停态可以改成更深的红色或金色看业务调性。光标建议用 IDC_HAND 手型光标用户一看就知道可交互。5. 柱状图避坑指南GDI 泄漏、坐标反转、闪烁和白边5.1 柱体倒挂Y 轴方向搞反现象柱子从绘图区顶部向下生长或者高度越大越往下沉。原因MFC 的窗口坐标 Y 轴正方向朝下业务数值越大应该对应屏幕 Y 坐标越小。如果直接用数值当高度去填 CRect 的 top就会画成倒挂。这是新手最常见的翻车点。解决计算柱体矩形时top rcPlot.bottom - nBarHeightbottom 固定等于 rcPlot.bottom。我习惯把 rcPlot 底部这条线理解成「业务零点」所有高度都从它向上减这个模型清晰后不会再有颠倒的问题。另外注意如果业务数据包含负值这个零点模型就不够用了要在避坑 5.4 里单独处理。5.2 拖动窗口后花屏缺少双缓冲现象拖动对话框或让窗口重绘时柱状图区域闪成白块或出现上一帧图像的残影。原因OnPaint 直接对屏幕 DC 逐个绘制图形窗口每次擦除背景和绘制图形交替进行速度慢时就会看到闪烁。这是 GDI 绘图的老问题。解决改用双缓冲绘制先在内存 DC 里画完整个画面再一次 BitBlt 贴到屏幕 DC。这个方案代码不多收益非常大。第六部分会给出完整实现如果你只看一处优化就选它。5.3 GDI 对象泄漏进程崩溃或系统卡死现象程序长时间运行后越来越卡任务管理器里 GDI 对象数量持续上涨最终绘图全部失败。原因OnPaint 里每帧都创建 CPen、CBrush、CFont如果只是 new 或 Create 而不 DeleteObject这些对象会一直占用系统内核资源直到进程退出才释放。绘图代码每帧泄漏 3~5 个对象半天下来就是几千个。解决三个原则一是画刷、画笔、字体尽量用局部变量离开作用域前手动 DeleteObject二是 SelectObject 返回的旧对象在使用完 Select 回来三是避免在循环里重复创建。如果你用了 5.1 的渐变绘制特别注意循环里的画刷每一帧都会创建 20 个对象不删的话半天就顶不住了。5.4 数据全为负或含负数柱子飞出了绘图区现象业务数据变成负值后柱子画到了 Y 轴左侧或者向下伸出绘图区底部。原因基础版的坐标换算假设数据最小值为 0。数据带负号时必须同时考虑最小值和最大值把绘制区间映射到 [minVal, maxVal] 的完整值域否则布局彻底错乱。解决坐标换算改为先找 minVal 和 maxVal零点在绘图区内的位置是 (maxVal - 0) / (maxVal - minVal)。正负柱子分别从零点向上和向下绘制。这个改法代码不复杂但要把测试数据覆盖到「全正」「全负」「有正有负」三个场景尤其是全负时零点应该在绘图区顶部。很多开发者在全正数据下跑得很流畅一上真实数据就翻车问题就在这。5.5 文字白底背景BkMode 没设置现象刻度数字或标签文字背后出现一块矩形白底盖住了网格线和柱子边缘。原因GDI 默认的 SetBkMode 是 OPAQUE文字绘制时会把背景像素填充为当前背景色。默认窗口背景色是白色在深色柱子上就会显示白色方块。解决绘制文字前调用 pDC-SetBkMode(TRANSPARENT)一次设置本帧内持续有效。注意每个使用字体的 DC 都要单独设置双缓冲里的内存 DC 也不例外——因为内存 DC 的初始状态和屏幕 DC 不同。6. 双缓冲完整实现与数据范围自适应最后一块拼图双缓冲是消除闪烁的标准做法核心就是一段固定的骨架代码内存 DC、兼容位图、绘完贴回。我把 OnPaint 的完整结构写在这里你可以直接替换上一章的基础版本。void CBarChartCtrl::OnPaint() { CPaintDC dc(this); CRect rcClient; GetClientRect(rcClient); // 内存 DC 和兼容位图 CDC memDC; memDC.CreateCompatibleDC(dc); CBitmap bmp; bmp.CreateCompatibleBitmap(dc, rcClient.Width(), rcClient.Height()); CBitmap* pOldBmp memDC.SelectObject(bmp); // 先用白色铺满背景这一步避免上一帧残留 memDC.FillSolidRect(rcClient, RGB(255, 255, 255)); // 所有绘制都在内存 DC 上进行 DrawChart(memDC); // 一次性贴回屏幕 dc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), memDC, 0, 0, SRCCOPY); // 恢复旧对象并释放 GDI 资源 memDC.SelectObject(pOldBmp); bmp.DeleteObject(); memDC.DeleteDC(); }逻辑说明CreateCompatibleDC 创建的内存 DC 和屏幕 DC 兼容CreateCompatibleBitmap 创建的位图也匹配当前屏幕格式保证 BitBlt 的效率最高。所有 DrawChart 内部的绘图代码都不需要改只是把传入的 DC 从屏幕 DC 换成内存 DC这就是双缓冲侵入性最小的原因。数据范围自适应是最后一个补齐项。基础代码里我用 dMaxVal 做归一化遇到负数就炸。自适应做法是先遍历一遍数据求出 minVal 和 maxVal再把绘图区高度按「最大值减最小值」的区间去映射零点位置按比例算出。全负数据时所有柱子从顶部向下生长视觉上方向依然正确。这个控件到这一步已经具备交付能力。回顾一下使用这个方案的完整习惯先想清楚数据范围是否存在负值其次把 GDI 对象的生命周期固化成肌肉记忆最后在第一次跑通后立刻把双缓冲加上。我见过太多项目在图上叠功能最后折在闪烁和泄漏上而不是绘图逻辑本身。希望这篇笔记能帮到一个正在和 MFC 自绘控件较劲的人。祝你今天写出的柱子每一根都在它该在的位置。本文还有配套的精品资源点击获取