资讯动态

MFC高速二维曲线控件:GDI优化与双缓冲实战

发布时间:2026/10/15 21:02:33 来源:尧图企业网站定制
简介面向MFC开发者的高速二维曲线控件示例程序基于ChartCtrl控件解决MFC缺少现成曲线显示组件的痛点适用于数据采集、实时波形显示等场景并已通过VC2017编译验证。压缩包共77个文件包含38个头文件、27个C源文件、4个inl内联模板文件及工程配置与资源文件整体仅245KB目录结构紧凑便于对照学习。已有1654人学习下载实用性得到一定认可。示例不仅演示了VC2017下取消预编译头、以Custom Control承载控件并设置0x52010000样式等关键操作还展示了曲线根据数据自动填满绘图区、静止状态下可拉框放大局部曲线等交互功能配套工程可直接编译运行适合需要快速集成曲线显示的MFC开发者参考复用。1. 标题是什么为什么“高速”二字让 MFC 曲线控件成了刚需看到“MFC高速二维曲线控件示例程序VC2017编译通过”这个标题多数人的第一反应是“画曲线嘛Polyline 一画就完事”。但真把采集数据丢进去就会发现数据量一过万界面帧率直接掉到个位数拖动窗口时曲线像心电图一样闪甚至直接假死。MFC 本身并不慢慢的是大多数人对 GDI 绘制的默认用法逐段画线、整窗重绘、每次刷新都做浮点坐标换算。这个方向的本质不是“画出一根曲线”而是“在数据持续写入的前提下把每次刷新压到几毫秒以内”。我早期做量测类上位机时也被这个问题卡过后来才把双缓冲、Polyline 批量绘制、区域失效刷新这几个套路固定下来。这篇笔记就沿着这个标题讲清楚这个控件该选什么绘制路径、代码怎么搭、参数怎么调、以及 VC2017 下最容易踩的五个坑。目标是让你照着复现出一个能塞进现有 MFC 工程的自绘曲线控件并在每秒几十帧的刷新场景下不闪、不卡、不泄漏。2. 先搞清楚三个卡点为什么 MFC 默认画曲线“慢”很多人以为曲线刷新慢是电脑性能不行其实在 MFC 里慢往往来自三个具体环节绘制原语太碎、重绘面积太大、坐标换算太频繁。这三件事不解决换再好的 CPU 也白搭。下面逐个拆开讲。2.1 绘制原语的选择不一样速度能差出 10 倍MFC 封装了 GDI而 GDI 画曲线最简单的方式是 MoveToEx 加 LineTo一段段把点连起来。假设一帧要画 5000 个点那就意味着要调用 4999 次 LineTo。每次 LineTo 又涉及 GDI 内部的线段初始化、参数校验和像素扫描转换这种碎片化调用在 GDI 里开销非常大。更重要的是LineTo 一次只能画一条线段GPU 加速几乎用不上GDI 大多数情况下是软件光栅化线段越多总耗时近似线性上涨。我用同样一组数据做过对比5000 个点用 MoveToEx LineTo 逐段画耗时大概在 8 到 12 毫秒改用 Polyline 一次调用传入 5000 个点耗时能压到 2 到 3 毫秒。差别不是一两倍而是三到五倍而且点越多差距越明显。核心原因在于 Polyline 把整条折线的计算聚合在了一个 GDI 调用里内部的顶点处理和线段生成可以复用同一套状态少了大量重复的上下文切换。所以标题里的“高速”二字落到绘制路径上的第一层意思就是能用 Polyline 就不要用 LineTo 循环。如果你的曲线还需要不同颜色分段那也别逐段 LineTo而是把同色段分别聚合成几个 POINT 数组分段调用 Polyline。这样既保留了颜色区分又尽量拉高单次调用效率。2.2 双缓冲不是可选项是默认前提MFC 控件默认的刷新逻辑是收到 WM_PAINT系统先发 WM_ERASEBKGND 让你擦背景擦完再执行 OnPaint 画内容。这两个动作发生在同一个屏幕上擦背景和画内容的间隙屏幕有一瞬间是露底的人眼看到的就是闪烁。数据量一上来每一帧处理时间变长闪烁就越明显。解决办法是双缓冲先在内存里创建一块和控件客户区一样大的位图把背景、坐标网格、曲线全部画到这张位图上画完之后用 BitBlt 一次把整张位图拷贝到屏幕上。屏幕上永远只有两个状态上一帧完整画面和这一帧完整画面不存在裸露背景的中间态。双缓冲的代价是多一次 BitBlt 的整块拷贝对于几百像素宽的曲线区这个拷贝耗时通常在亚毫秒级别远小于闪烁带来的体验损失。我一般会在 OnPaint 里固定走“内存 DC 画图、BitBlt 上屏”的流程不管数据量大小统一用这个结构。这样代码逻辑也简单OnPaint 里所有绘制代码只需要面对内存 DC不关心屏幕刷新时机。2.3 坐标换算也占 CPU浮点除法是隐形开销第三个卡点最容易忽略。绘制曲线前要把每个数据值从“业务坐标”映射成“屏幕像素坐标”。常见写法是 yPixel (data - yMin) / (yMax - yMin) * height看起来就一行公式但数据量一上万这一行公式就要执行上万次而且里面全是浮点除法。浮点除法比加减法慢一个数量级一万次下来可能就是几毫秒的纯 CPU 消耗。优化思路是把除法提出来变成一次除法加多次乘加。预先计算缩放比例 scaleY height / (yMax - yMin)再计算偏移量 offsetY -yMin * scaleY那么 yPixel data * scaleY offsetY。浮点乘法比除法快得多尤其在现代 x86 处理器上差距明显。这条优化看起来不起眼但数据量大时能省下不少时间属于典型的“一行代码提升一个数量级”操作。这一节的结论先立住高效曲线绘制的三板斧是Polyline 批量绘制、双缓冲上屏、坐标换算简化。后面所有代码和调优都围绕这三件事展开。3. 在 VC2017 里把控件搭起来从工程向导到第一根曲线在开始写代码之前先把工程环境说清楚。标题里说“VC2017 编译通过”实际操作时对应的是 Visual Studio 2017 自带的 v141 平台工具集。下面从工程配置、控件类声明、数据缓冲到绘制代码一步步把这个控件跑起来。3.1 工程配置与字符集选择新建 MFC 应用程序时建议选择“基于对话框”或“单文档”这个选择影响不大关键是工程属性里这几项平台工具集选择Visual Studio 2017 (v141)。字符集统一用Unicode。MFC 新版本默认就是 Unicode避免用多字节字符集时要处理各种 TCHAR 宏。在预处理定义里保留_AFXDLL对应“在共享 DLL 中使用 MFC”的运行时链接方式。如果你打算把控件封装到单独模块注意包含 MFC 头文件时先后顺序别乱公共头文件最好放在 pch.h旧工程是 stdafx.h里不然会出现一堆莫名其妙的类型未定义错误。一个容易忽略的点是VS2017 默认创建的新工程直接用 pch.h 作为预编译头而网上很多旧代码示例写的是 stdafx.h。如果你拿到一个老示例编译报错“找不到 stdafx.h”直接新建一个空的 pch.h把内容补上即可。3.2 控件类声明从 CWnd 派生而不是 CStatic做自绘曲线控件可以直接从 CWnd 派生一个子类。不要从 CStatic 派生因为 CStatic 的语义是“静态文本”虽然也能自绘但它和对话框的资源绑定关系会带来很多不必要的限制比如你很难在运行时动态创建、动态销毁。我一般是这样声明控件类的// CurveCtrl.h #pragma once #include afxwin.h #include vector class CurveCtrl : public CWnd { public: CurveCtrl(); virtual ~CurveCtrl(); BOOL Create(DWORD dwStyle, const CRect rc, CWnd* pParent, UINT nID); void AppendData(double dVal); void SetRange(double yMin, double yMax); void ClearData(); int GetDataCount() const { return (int)m_data.size(); } protected: afx_msg void OnPaint(); afx_msg BOOL OnEraseBkgnd(CDC* pDC); afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); DECLARE_MESSAGE_MAP() private: std::vectordouble m_data; double m_yMin; double m_yMax; BOOL m_bRangeSet; };这里有几个设计点说明一下。派生自 CWnd意味着你可以用 CWnd::Create 在运行时动态创建控件也可以把它放进对话框模板然后通过 DDX 绑定的方式关联。AppendData是数据入口外部只需要把新的采样值往里丢控件内部管理缓冲。SetRange用来固定曲线纵轴范围如果外部不调用控件会自己根据数据最大最小值动态缩放。成员变量m_bRangeSet标记是否手动设置过范围避免每次刷新都重复计算纵轴。3.3 数据入口与缓冲管理数据缓冲是高速刷新的前提。如果每来一个数据就触发一次控件重绘刷新效率会非常低因为重绘画面的开销远大于插入一个点的开销。常见做法是数据先存入 std::vector等累积到一定数量后统一刷新。我一般用两种策略实时显示模式每次 AppendData 后如果数据量小于设定的最大点数就调用 InvalidateRect(FALSE) 触发局部刷新。批量显示模式数据先攒在缓冲区里攒到比如 512 个点再一次性刷新。AppendData 的实现可以这样写void CurveCtrl::AppendData(double dVal) { m_data.push_back(dVal); // 控制最大点数防止无限增长拖慢绘制 if ((int)m_data.size() MAX_POINTS) { // 这里只保留最近 MAX_POINTS 个点 m_data.erase(m_data.begin(), m_data.end() - MAX_POINTS); } // 触发重绘注意 FALSE 表示不擦除背景配合 OnEraseBkgnd 的性能优化 InvalidateRect(NULL, FALSE); }参数说明MAX_POINTS是一个编译期常量我一般设为控件像素宽度的两倍具体原因在下一章讲抽稀时详述。InvalidateRect(NULL, FALSE)的第二个参数是 bEraseFALSE 表示不擦除背景这能减少一次背景填充操作。前提是你在 OnEraseBkgnd 里也返回 TRUE 不做擦除否则这里填不填 FALSE 差别不大。这个实现有两个细节需要注意。第一erase操作在 vector 头部删除元素时会把后面的数据整体前移如果 MAX_POINTS 较大且每次只删一两个点这个操作也是有一定开销的。如果发现这块耗时明显可以换成环形缓冲ring buffer用一个头索引和尾索引维护窗口范围避免频繁搬移数据。第二每次 AppendData 都 InvalidateRect 会把重绘请求合并到消息队列里Windows 会聚合同一时刻的多个无效区域从而避免“数据一多消息一多每一帧都画不完”的恶性循环。3.4 双缓冲绘制三段式 OnPaint 流程OnPaint 是整个控件的核心。它的结构永远是三段创建内存 DC、在内存里画、一次性 BitBlt 上屏。下面给出一个可直接抄的版本。void CurveCtrl::OnPaint() { CPaintDC dc(this); CRect rcClient; GetClientRect(rcClient); // 第一步创建兼容内存 DC 和位图 CDC memDC; CBitmap memBmp; memDC.CreateCompatibleDC(dc); memBmp.CreateCompatibleBitmap(dc, rcClient.Width(), rcClient.Height()); CBitmap* pOldBmp memDC.SelectObject(memBmp); // 第二步填充背景 memDC.FillSolidRect(rcClient, RGB(255, 255, 255)); // 第三步画数据曲线 int nCount (int)m_data.size(); if (nCount 1) { // 分配 POINT 数组长度不超过像素宽度 int nPtCount min(nCount, rcClient.Width()); std::vectorPOINT pts(nPtCount); double yMin m_bRangeSet ? m_yMin : *min_element(m_data.begin(), m_data.end()); double yMax m_bRangeSet ? m_yMax : *max_element(m_data.begin(), m_data.end()); double spanY (yMax - yMin) 0.0 ? (yMax - yMin) : 1.0; double scaleY (double)(rcClient.Height() - 1) / spanY; // 从数据尾部取最近 nPtCount 个点保证显示的是最新曲线 int iStart nCount - nPtCount; for (int i 0; i nPtCount; i) { double dVal m_data[iStart i]; pts[i].x i; pts[i].y (int)(((yMax - dVal) * scaleY) 0.5); if (pts[i].y 0) pts[i].y 0; if (pts[i].y rcClient.Height()) pts[i].y rcClient.Height() - 1; } // 批量绘制一次调用替代 LineTo 循环 memDC.Polyline(pts.data(), nPtCount); } // 第四步整体拷回屏幕 dc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), memDC, 0, 0, SRCCOPY); // 第五步还原位图释放 GDI 资源 memDC.SelectObject(pOldBmp); // memBmp 和 memDC 的生命周期在函数结束时自动释放 }逻辑说明这里画曲线时先根据控件宽度截取可见范围内的点数。如果数据超过了像素宽度就优先显示最新一段这是滚动曲线最常见的策略。y 坐标计算用了缩放后的一次乘法和一次加法没有每帧重复除法。最后 BitBlt 用的是 SRCCOPY 光栅操作直接覆盖目标区域不做透明处理保证整帧一致。参数说明(int)(((yMax - dVal) * scaleY) 0.5)这里的 yMax - dVal 把纵轴倒转过来左上角是纵轴最大值符合屏幕坐标习惯。0.5 是做四舍五入而不是直接截断避免曲线出现系统性下移一个像素的偏差。边界裁剪的 if 判断是必须的当数据超出设置范围时直接越界画到控件外面会干扰其他区域甚至在禁用裁剪标志时产生不可预期的画布内容。4. 把每秒刷新次数提上去四个参数按这个调控件跑通之后下一个问题就是性能。同样的代码参数设置不同帧率能差 3 倍。这章讲我实际调曲线控件时最常动的四个参数每个都有自己的适用范围和边界条件。4.1 数据点数上限必须小于等于控件宽度像素这是最容易被忽略的一条。假设控件宽度是 600 像素而你画了 10000 个点那么在不做抗锯齿压缩的情况下横轴只有 600 个像素位置最多只能呈现 600 个不同 x 坐标。多余的 9400 个点全部画在了重叠区域内纯粹是浪费计算。正确做法是在绘制前把数据抽稀只保留能对应到像素列上的点。最简单的抽稀方式是等间隔采样也就是代码里的 nPtCount min(nCount, rcClient.Width())。更讲究一点的做法是找每个像素列内的最大最小值画成竖线这样不会漏掉极值。但那个占用的耗时比较大如果你的数据本身是平滑的物理量直接等间隔采样就够了。我习惯把最大点数直接限制在控件宽度同时配合一个总缓冲上限。比如控件宽 800 像素就把数据缓冲上限设为 1600取最近 800 点做绘制这样缩放曲线时能看到一个波形的完整周期本地存储和绘制开销都可控。4.2 预先把 double 数组转成 POINT 数组每次重绘时如果都从 double 数组重新计算坐标即使做了优化也是一遍纯计算。对于高速刷新场景还可以再进一步在数据写入阶段就维护一个映射好的 POINT 缓冲。每次 AppendData 时同时把新数据转换成屏幕坐标塞进 POINT 数组OnPaint 里只做 Polyline不再做任何坐标计算。这个做法的好处是显而易见的OnPaint 时间被压缩到“批量拷贝传入 GDI BitBlt”两个阶段对于 800 点以内的曲线绘制耗时可以稳定保持在 0.5 毫秒以内。代价是当控件尺寸或 y 轴范围变化时需要做一次全量坐标重建。我一般会在 OnPaint 里判断控件尺寸是否有变化如果变了就重新转换全部坐标否则直接用缓存数据。取舍建议是如果你的数据写入频率远高于刷新频率优先用“写入时转换”策略如果数据是查询式的很久才变一次每次查询又要拖动缩放那就在 OnPaint 里现算避免维护两份数据带来的一致性麻烦。4.3 只刷新变化区域InvalidateRect 矩形参数别乱填默认情况下大家都会在 AppendData 后调用 Invalidate()这会整窗重绘。如果控件本身只有曲线区整窗重绘和局部重绘差异不大。但当曲线控件旁边还有坐标轴、图例、数值显示区时整窗重绘会把这些区域全部刷新且刷新顺序不可控容易出现图例先闪一下、曲线再出现的视觉割裂。更细的做法是只把“新数据延伸到的区域”设置为无效区域。比如原来的曲线画到了宽度一半的位置新增了 10 个点其实只有从原来半位置到半位置再偏右 10 个像素的窄条需要重绘。用 InvalidateRect(rect, FALSE) 传入这个窄条矩形系统只发送这一小块的 WM_PAINT绘制时再用 BitBlt 把新画面贴上去。不过要注意这种局部刷新只能用在“曲线只在右侧增长”的模式下如果整条曲线全量重绘比如滚动波形局部失效的收益就不大了。这里有个容易翻车的点双缓冲 BitBlt 时如果只贴一小块其他区域保留原有内容Pinball 表面新块和旧块之间的边界可能会出现一条细细的接缝。数据剧烈抖动时这个现象很明显。我的做法是小区域更新时把矩形适当右扩 1 到 2 个像素让新旧帧之间有重叠减少接缝感。4.4 关掉背景擦除免得每帧多干一次活默认的 WM_ERASEBKGND 消息会触发控件背景填充通常用白色或对话框底色填充整个客户区。前面讲过OnPaint 第一次画到内存 DC 时已经填充了背景屏幕上的背景擦除形成了一个冗余步骤。标准做法是在控件类里拦截 WM_ERASEBKGND直接返回 TRUE告诉系统“背景不用你管我自己处理”。BOOL CurveCtrl::OnEraseBkgnd(CDC* /*pDC*/) { // 返回 TRUE 表示背景已被清除系统不再执行默认擦除 return TRUE; }这个函数的返回值有讲究返回 TRUE 表示已处理返回 FALSE 表示继续执行系统默认擦除。很多人直接把函数体留空但那相当于返回 FALSE系统还会继续擦背景。正确写法是函数体就一个 return TRUE注意去掉参数名避免 C4100 警告。除了关掉背景擦除还有一个参数值得检查画笔的默认参数。很多人从上一段代码复制了画笔和画刷没有释放画笔种类用默认的 PS_SOLID 也可以高性能场景下不要用 PS_DASH 或 PS_DOT虚线点线在 GDI 里属于“几何画笔”绘制开销比实心线大不少。如果你的曲线本身不需要区分线型统一用实心线最省。5. 常见踩坑VC2017 编译与运行的五段血泪记录代码写完不等于能跑编译通过不等于不崩溃。这一章整理过去实际调试中遇到最多的五个问题每条都按照现象、原因、解决的顺序写清楚遇到时直接对照排查。5.1 现象编译报错 LNK2019外部符号无法解析在一台新装的 VS2017 环境里编译 MFC 工程最常见的就是“error LNK2019: unresolved external symbol”而且往往指向 WinMain 或 wWinMain。最初还以为是代码写错了折腾很久后来发现是工程配置里“链接器-系统-子系统”被设成了“控制台”而 MFC 程序入口是窗口子系统。另一个高发原因是字符集不匹配MFC 的动态库用的是 Unicode 版本工程却配成了多字节字符集导致链接器找不到对应函数签名。解决方法右键工程 - 属性 - 链接器 - 系统 - 子系统选“Windows”再把“常规-字符集”改成“使用 Unicode 字符集”重新生成一遍就过了。如果还报 stdafx.h 找不到那是因为 VS2017 默认预编译头是 pch.h可以在工程属性里把“预编译头”关掉或者建一个空 pch.h 顶上去。5.2 现象曲线像心电图一样闪烁窗口拖起来全是残影闪烁是最容易判断的问题通常是没做双缓冲或者做了双缓冲但 BitBlt 时机不对。典型错误是直接在 OnPaint 里用 CPaintDC 往屏幕上画画一条 LineTo 刷一条屏幕频繁切换前景背景。另一个可能原因是 OnEraseBkgnd 漏写导致系统先擦背景、再画曲线两步之间有时间差。解决分两步第一在 OnEraseBkgnd 里 return TRUE第二OnPaint 里严格走“内存 DC 绘制 - BitBlt 上屏”。如果做完这两步还闪检查一下是否设置了 CS_HREDRAW 和 CS_VREDRAW 窗口类样式这两个样式的存在会在尺寸变化时强制整窗重绘正是闪烁的来源之一。不需要的控件可以在创建时把样式里这两个标志去掉。5.3 现象任务管理器里 GDI 对象数直线上升程序变慢这是一个隐蔽的资源泄漏。OnPaint 里创建了 CBitmap 和 CDC按理说生命周期到函数结束时应该自动释放但很多人忽略了一个细节SelectObject 返回的旧对象必须恢复否则兼容位图在内存里一直处于“被选中”状态DeleteObject 不成功每次重绘都泄漏一张位图。这种泄漏不显眼但程序跑几十分钟后曲线开始变慢最终 GDI 对象耗尽绘不出来。解决记住保存 SelectObject 的返回值函数退出前再 SelectObject 回去之后再让 CBitmap 和 CDC 随作用域销毁。如果你把代码写成了 CreateCompatibleBitmap 之后忘了选回旧对象又或者重复 SelectObject 了两次都会导致资源持有计数不符。检查方式很简单在 OnPaint 末尾数一数局部变量的析构顺序确保位图和 DC 还在生命周期内。5.4 现象高分屏上曲线模糊、鼠标点选坐标总是偏几像素这个问题在 Win10 高分屏上特别常见。默认情况下进程没有声明 DPI 感知Windows 会对整个窗口做位图缩放看起来就是“字体锐利但曲线发虚”而且鼠标坐标和绘制坐标不一致。如果控件还涉及到点击选点功能偏的像素一多完全对不上。解决在程序启动时调用一次 DPI 感知设置并按照实际 DPI 缩放比例重新计算控件尺寸。用 MFC 的话可以在 CWinApp::InitInstance 里::SetProcessDPIAware();如果用的是 VS2017 自带的 MFC 向导右键工程 - 属性 - 配置属性 - 清单工具 - 输入输出 - DPI 感知选“Per Monitor High DPI Aware”。这样窗口以物理像素绘制曲线不会虚鼠标坐标换算也回归一一对应。5.5 现象数据量过万后拖动窗口直接卡死最后一个坑业务线程每来一个数据点就调用一次 AppendDataAppendData 里又直接 InvalidateRect导致 UI 线程消息队列里堆满了重绘请求。每个重绘请求处理时又要遍历上万个点换算坐标处理完一个又来一个UI 线程再无暇处理鼠标拖动窗口的消息视觉上就是卡死。解决思路是把数据写入与刷新解耦AppendData 只往缓冲区写数据不在数据入口触发重绘重绘由定时器或单独的信号触发频率限制在每秒 30 帧以内。每帧只处理当前缓冲区里的数据如果某个时刻数据量太大就做抽稀保证绘制时间恒定。这样即使数据峰值到了几十万点界面依旧响应迅速只是刷新率不变。6. 从能画到画得快验证手段与两个进阶玩法6.1 用性能计数器量化每次绘制耗时高速控件的性能不能靠“看起来不卡”做判断得量化。我习惯在 OnPaint 结束前用 QueryPerformanceCounter 统计一次实际绘制耗时并把它输出到调试窗口或者一个变量里。基线指标是1000 点以内的曲线单帧总耗时小于 2 毫秒5000 点抽稀后小于 3 毫秒滚动刷新时持续稳定在 30 帧以上。如果达不到优先检查是不是坐标换算太重或者绘制 API 用成了 GDI。LARGE_INTEGER freq, t1, t2; QueryPerformanceFrequency(freq); QueryPerformanceCounter(t1); // 原有绘制代码 ... QueryPerformanceCounter(t2); double dMs (double)(t2.QuadPart - t1.QuadPart) * 1000.0 / freq.QuadPart;这个测量不放进发布代码但调试时必须保留因为性能优化如果缺少数据支撑很容易调了半天都耗在无关紧要的环节上。6.2 进阶玩法一环形缓冲让滚动曲线不搬数据当数据是持续流式写入时vector 的头部删除会引起内存搬移。进阶做法是预先分配固定大小数组用一个写索引循环覆盖最旧数据。这样 AppendData 的耗时变成 O(1)无论跑多久缓冲区的性能都不退化。具体实现不复杂只是读数据时不再从 vector 的 begin 顺序读而是按写索引往前倒推固定数量。配合 Polyline 批量绘制能把这个控件用在长时间运行的采集场景里。6.3 进阶玩法二定时刷新模式有些场景数据更新频率不固定比如串口一帧来了 50 个点下一帧要等 200 毫秒后才来。每个点都触发重绘不值当。我的习惯是高频数据用 30 毫秒定时器合并刷新低频数据用“达到 N 点才刷一次”的批量门槛。两者结合后界面刷新次数固定数据吞吐量再大也只是在缓冲区里堆积不会把 UI 线程拖垮。做完这步这个控件才算真正配得上“高速”两个字。做这类控件养成的习惯是永远不在数据回调里直接绘制永远不整窗重绘无变化区域永远用计数器说话。把这三个“永远”守住MFC 曲线控件的性能问题就解决了一大半剩下的都是具体的边界参数调整。希望这篇笔记能帮你少走几次弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑