简介这份源码资源面向具备一定C基础的Windows开发者尤其是正在学习MFC框架、需要在桌面应用中快速实现数据可视化的工程师与学生。它通过一个可运行的对话框工程演示如何借助ChartCtrl控件完成波形数据的绘制与实时刷新解决传统MFC绘图繁琐、上手慢的问题。压缩包共80个文件约41.86MB以h头文件与cpp源文件为主体辅以inl、ico、rc资源脚本及vcxproj、sln工程配置完整保留了Visual Studio 2015企业版下的编译结构。源码覆盖控件初始化、数据组织、坐标轴设定、定时器刷新与鼠标交互事件处理等关键环节并包含界面美化与图例提示等细节读者可据此理解折线波形从数据到呈现的完整链路。目前已有3852人学习下载适合作为MFC图表开发的入门范例与二次开发起点。1. 从一块“画不出波形”的工控屏说起前阵子帮一个做数据采集的朋友看代码他那个 MFC 上位机跑起来后界面上本该实时刷新的波形区域一直是一片空白数据明明已经进到缓冲区了就是画不出来。他原本用的是 Picture Control 加一堆 GDI 手绘坐标换算、双缓冲、刷新频率全得自己扛改到后面连自己都绕晕了。后来我让他换成 ChartCtrl 这个 MFC 的图表控件半天时间就把波形显示跑通了。这份源码实例干的就是这件事在 MFC 对话框程序里用 ChartCtrl 快速搭出一个能实时显示波形的界面适合做工业采集、串口调试、传感器监控这类上位机的开发者。它不追求花哨核心就是让你把“数据进来、波形出去”这条链路最短化省掉手绘坐标轴和刷新逻辑的血泪经验。2. 项目定位与技术选型为什么是 ChartCtrl 而不是手绘2.1 这个实例到底解决什么问题做 MFC 上位机的人大多遇到过同一个困境界面框架用 MFC 搭得挺快一到波形显示就卡住。用 Picture Control 自己画意味着你要处理坐标映射、刻度计算、网格线、双缓冲防闪烁、鼠标缩放甚至还要考虑多通道曲线的颜色管理。这些活儿不是不能干但每个项目都重写一遍纯属浪费。这份源码的思路很直接把波形显示这件事交给一个成熟的图表控件 ChartCtrlMFC 只负责数据采集和界面布局。ChartCtrl 本身是一个开源的 MFC 图表控件库支持折线、散点、柱状、面积等多种图表类型自带坐标轴、网格、图例和缩放。实例里重点演示的是折线波形也就是最常见的实时数据展示场景。它适合的人群很明确正在做或准备做 MFC 上位机、需要显示实时曲线、又不想在绘图上耗太多时间的开发者。新手可以拿它当模板直接改数据源熟手可以看它的控件封装方式和刷新机制再决定要不要做二次封装。2.2 技术栈与依赖关系这个实例的技术栈不复杂但有几个点需要提前说清楚否则编译阶段就容易翻车。项目说明开发语言C界面框架MFC对话框程序为主图表控件ChartCtrl开源库需集成到工程开发环境Visual Studio建议 2015 及以上字符集建议统一用 Unicode避免字符串转换问题数据来源实例中用模拟数据实际项目替换为采集线程或串口回调ChartCtrl 不是 VS 自带的控件需要把它的源码文件加入工程。常见做法是把 ChartCtrl 相关的 .h 和 .cpp 文件放到一个独立目录然后在项目里添加现有项或者编译成静态库再引用。实例源码里通常已经包含这些文件你拿到后先确认目录结构是否完整。提示ChartCtrl 的源码文件在不同版本里命名可能略有差异集成时以实例里实际包含的文件为准不要凭记忆去网上找同名文件替换。2.3 目录结构长什么样拿到源码后先别急着编译花两分钟看一下目录结构能省掉后面很多“找不到文件”的麻烦。一个典型的 ChartCtrl 波形显示实例目录大致是这样的ChartWaveDemo/ ├── ChartCtrl/ # 图表控件源码目录 │ ├── ChartCtrl.h │ ├── ChartCtrl.cpp │ ├── ChartAxis.h │ ├── ChartAxis.cpp │ ├── ChartLineSerie.h │ ├── ChartLineSerie.cpp │ └── ... # 其他坐标轴、图例、网格相关文件 ├── res/ # 资源文件 │ ├── ChartWaveDemo.rc2 │ └── ChartWaveDemo.ico ├── ChartWaveDemo.h # 主对话框头文件 ├── ChartWaveDemo.cpp # 主对话框实现 ├── ChartWaveDemoDlg.h # 对话框类头文件 ├── ChartWaveDemoDlg.cpp # 对话框类实现核心逻辑在这里 ├── stdafx.h / pch.h # 预编译头 └── ChartWaveDemo.sln # 解决方案文件核心逻辑集中在对话框类的 .cpp 文件里ChartCtrl 目录是控件本体一般不需要改动。如果你要移植到自己的项目把 ChartCtrl 目录整体拷过去再把对话框里的初始化代码和刷新代码搬过去就行。2.4 控件初始化的关键步骤波形显示能不能跑起来第一步就看控件有没有初始化对。实例里通常会在对话框的OnInitDialog里完成 ChartCtrl 的绑定和基本设置。下面这段代码是典型的初始化流程// 在对话框头文件中声明控件变量 // CChartCtrl m_ChartCtrl; BOOL CChartWaveDemoDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 1. 将 ChartCtrl 控件与对话框上的占位区域绑定 // 实例中通常用 Picture Control 作为占位再子类化为 CChartCtrl m_ChartCtrl.SubclassDlgItem(IDC_CHART_PLACEHOLDER, this); // 2. 创建图表对象一个 ChartCtrl 可以包含多个图表 m_ChartCtrl.CreateChart(); // 3. 获取默认图表的指针后续所有曲线都加在这个图表上 CChartCtrl* pChart m_ChartCtrl.GetChart(); // 4. 设置坐标轴范围X 轴 0~100Y 轴 -1.5~1.5 pChart-GetBottomAxis()-SetMinMax(0, 100); pChart-GetLeftAxis()-SetMinMax(-1.5, 1.5); // 5. 开启网格显示方便观察波形幅度 pChart-GetBottomAxis()-SetGridVisible(true); pChart-GetLeftAxis()-SetGridVisible(true); // 6. 创建一条折线序列并设置颜色和线宽 CChartLineSerie* pLineSerie pChart-CreateLineSerie(); pLineSerie-SetColor(RGB(0, 200, 0)); pLineSerie-SetWidth(2); return TRUE; }这段代码的逻辑说明一下。SubclassDlgItem是把对话框上的一个静态区域变成 ChartCtrl 控件实例里一般会在资源编辑器里放一个 Picture ControlID 对应IDC_CHART_PLACEHOLDER。CreateChart负责创建内部图表结构GetChart拿到图表指针后才能操作坐标轴和曲线。坐标轴范围要根据你的实际数据来设设得太小波形会溢出设得太大波形会挤成一条线。CreateLineSerie创建一条折线序列返回的指针后面用来追加数据点。参数方面SetMinMax的两个参数分别是轴的最小值和最大值SetGridVisible控制网格线是否显示SetColor和SetWidth控制曲线颜色和线宽。这些都可以在运行时动态改比如做缩放时重新设坐标轴范围。3. 数据刷新与波形绘制从缓冲区到屏幕的完整链路3.1 数据是怎么进到曲线里的初始化只是把画布准备好真正让波形动起来的是数据刷新。实例里通常用一个定时器或者线程来模拟数据采集然后调用 ChartCtrl 的接口把新数据点追加到曲线上。下面是一个典型的定时器刷新逻辑// 在对话框类中定义定时器 ID #define TIMER_REFRESH 1 // 启动定时器间隔 50ms即 20 帧每秒 SetTimer(TIMER_REFRESH, 50, nullptr); void CChartWaveDemoDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent TIMER_REFRESH) { // 1. 模拟采集一个数据点实际项目中替换为真实数据源 static double x 0; double y sin(x * 0.1) * 1.0; // 模拟正弦波 // 2. 获取曲线序列指针实际项目中应保存为成员变量 CChartLineSerie* pLineSerie m_ChartCtrl.GetChart()-GetLineSerie(0); // 3. 追加数据点 pLineSerie-AddPoint(x, y); // 4. X 轴递增控制波形滚动 x 1.0; // 5. 如果数据点超过 100 个移除最早的点保持波形滚动效果 if (pLineSerie-GetPointsCount() 100) { pLineSerie-RemovePoint(0); m_ChartCtrl.GetChart()-GetBottomAxis()-SetMinMax(x - 100, x); } // 6. 刷新图表显示 m_ChartCtrl.RefreshCtrl(); } CDialogEx::OnTimer(nIDEvent); }这段代码的核心是AddPoint和RefreshCtrl。AddPoint往曲线里追加一个数据点参数是 X 和 Y 坐标。RefreshCtrl触发重绘把新数据画到屏幕上。如果不调用RefreshCtrl数据虽然进去了但界面不会更新这就是很多人遇到的“数据有但波形不动”的原因。滚动效果是通过移除最早的数据点并同步调整 X 轴范围实现的。GetPointsCount返回当前曲线上的点数RemovePoint(0)移除第一个点SetMinMax把 X 轴范围往后推。这样波形看起来就是从左往右滚动的。注意RefreshCtrl调用频率不要太高50ms 一次比较稳妥。如果设成 10ms 一次界面线程会被重绘拖慢反而导致数据采集卡顿。3.2 多通道波形怎么加实际项目里经常要同时显示多条曲线比如三轴加速度、多路温度。ChartCtrl 支持在一个图表里加多条折线序列每条序列独立设置颜色和线宽。下面是在初始化阶段加三条曲线的写法// 创建三条曲线分别对应 X、Y、Z 三轴数据 CChartLineSerie* pSerieX pChart-CreateLineSerie(); pSerieX-SetColor(RGB(255, 0, 0)); // 红色 pSerieX-SetWidth(2); CChartLineSerie* pSerieY pChart-CreateLineSerie(); pSerieY-SetColor(RGB(0, 255, 0)); // 绿色 pSerieY-SetWidth(2); CChartLineSerie* pSerieZ pChart-CreateLineSerie(); pSerieZ-SetColor(RGB(0, 0, 255)); // 蓝色 pSerieZ-SetWidth(2);刷新的时候分别往三条曲线里追加数据// 假设从传感器读到了三轴数据 double ax ReadSensorX(); double ay ReadSensorY(); double az ReadSensorZ(); pSerieX-AddPoint(x, ax); pSerieY-AddPoint(x, ay); pSerieZ-AddPoint(x, az); m_ChartCtrl.RefreshCtrl();多通道的关键是每条曲线单独管理数据点不要混在一起。如果通道数很多建议把曲线指针存到数组或 vector 里刷新时遍历追加避免写一堆重复代码。3.3 坐标轴和网格的细节调整坐标轴设置直接影响波形的可读性。实例里默认的坐标轴可能不够用实际项目中经常需要调整刻度间隔、标签格式和网格密度。下面是一些常用的调整代码// 设置 X 轴刻度间隔为 10 pChart-GetBottomAxis()-SetTickLength(10); // 设置 Y 轴刻度间隔为 0.5 pChart-GetLeftAxis()-SetTickLength(0.5); // 设置坐标轴标签的字体大小 pChart-GetBottomAxis()-SetFontSize(12); pChart-GetLeftAxis()-SetFontSize(12); // 隐藏右侧和顶部坐标轴只保留左侧和底部 pChart-GetRightAxis()-SetVisible(false); pChart-GetTopAxis()-SetVisible(false);SetTickLength控制刻度之间的间隔设得太密标签会重叠设得太疏波形细节看不清。SetFontSize调整标签字体在高分屏上适当调大能避免文字太小。隐藏多余的坐标轴可以让界面更干净尤其是小尺寸的显示区域。3.4 性能优化的几个实用手段波形刷新频率高了之后性能问题会逐渐暴露。界面卡顿、数据延迟、CPU 占用飙升这些都是常见现象。下面几个手段是实际项目中比较有效的第一控制刷新频率。不要每来一个数据点就刷新一次而是攒一批再刷。比如采集线程每 10ms 采一个点但界面每 50ms 刷一次一次追加 5 个点。这样重绘次数减少界面流畅度反而更好。第二限制曲线上的点数。波形显示不需要保留全部历史数据只保留当前屏幕能显示的点数就够了。比如 X 轴范围是 100那就只保留最近 100 个点超出的移除。这样RefreshCtrl的重绘量是恒定的不会越跑越慢。第三避免在刷新线程里做耗时操作。数据采集和界面刷新最好分开采集线程只管往缓冲区写数据界面定时器只管从缓冲区读数据并刷新。不要在定时器里做文件读写、网络请求这类阻塞操作。第四合理使用双缓冲。ChartCtrl 内部一般已经做了双缓冲但如果你在对话框上还有其他自绘元素注意不要破坏双缓冲机制否则会闪屏。4. 避坑与常见问题排查4.1 控件绑定失败程序启动就崩现象对话框初始化时调用SubclassDlgItem直接崩溃或者控件区域显示异常。原因占位控件的 ID 不对或者占位控件不是 Picture Control 类型。ChartCtrl 需要绑定到一个有效的窗口句柄上如果 ID 写错或者控件类型不匹配子类化就会失败。解决检查资源编辑器里占位控件的 ID 是否和代码里一致确认控件类型是 Picture Control 或 Static。如果还是不行改用Create动态创建控件不依赖资源编辑器里的占位。4.2 数据进去了但波形不显示现象调试时看到AddPoint被调用了数据也确实加进去了但界面上就是一片空白。原因最常见的是忘了调用RefreshCtrl或者坐标轴范围设错了数据点全在可视区域外面。还有一种情况是曲线颜色和背景色一样看起来像没画。解决先确认RefreshCtrl有没有调用再检查坐标轴的SetMinMax范围是否覆盖了数据点的值。如果都正常把曲线颜色改成红色试试排除颜色问题。4.3 波形刷新时闪屏严重现象波形每次刷新都闪一下看起来很不舒服。原因重绘时没有做双缓冲或者对话框背景擦除和图表重绘冲突了。解决在对话框的OnEraseBkgnd里直接返回 TRUE阻止背景擦除。同时确认 ChartCtrl 内部的双缓冲是否开启。如果对话框上还有其他控件尽量把图表区域独立出来减少重绘范围。4.4 长时间运行后界面越来越卡现象程序刚启动时波形很流畅跑了几分钟后越来越卡CPU 占用也越来越高。原因曲线上的数据点没有清理越积越多每次重绘都要画全部点。或者定时器里做了耗时操作导致消息队列堆积。解决在追加数据点时判断点数上限超过就移除最早的点。同时检查定时器回调里有没有阻塞操作把耗时逻辑移到独立线程。刷新频率也不要设得太高50ms 到 100ms 之间比较合适。4.5 移植到新项目后编译报错现象把 ChartCtrl 目录和对话框代码拷到新项目后编译一堆链接错误或头文件找不到。原因新项目的字符集、MFC 版本或预编译头设置和原实例不一致。ChartCtrl 源码里可能用了某些特定版本的 MFC 特性。解决先统一字符集建议都用 Unicode。再检查预编译头设置把 ChartCtrl 的 .cpp 文件也纳入预编译头管理。如果还有链接错误看看是不是漏加了某些 .cpp 文件到项目里。5. 二次开发改造从实例到自己的波形工具拿到这个实例后最直接的改造就是把模拟数据换成真实数据源。如果你用的是串口采集可以在串口回调里解析出数据然后通过自定义消息发给界面线程界面线程收到消息后追加数据点并刷新。如果用的是采集卡通常在驱动回调里拿到数据同样通过消息或线程安全队列传给界面。一个比较实用的改造方向是封装一个波形管理类把 ChartCtrl 的初始化、曲线创建、数据追加、坐标轴调整都包进去对外只暴露AddData(channel, value)和SetRange(min, max)这样的接口。这样界面代码会干净很多换控件或换项目时也容易迁移。下面是一个简单的封装示例class CWaveformManager { public: void Init(CChartCtrl* pCtrl, int channelCount) { m_pCtrl pCtrl; m_pChart pCtrl-GetChart(); m_pChart-GetBottomAxis()-SetMinMax(0, 100); m_pChart-GetLeftAxis()-SetMinMax(-1.5, 1.5); for (int i 0; i channelCount; i) { CChartLineSerie* pSerie m_pChart-CreateLineSerie(); pSerie-SetColor(GetChannelColor(i)); pSerie-SetWidth(2); m_series.push_back(pSerie); } } void AddData(int channel, double x, double y) { if (channel 0 || channel (int)m_series.size()) return; m_series[channel]-AddPoint(x, y); // 保持点数上限实现滚动效果 if (m_series[channel]-GetPointsCount() 100) { m_series[channel]-RemovePoint(0); m_pChart-GetBottomAxis()-SetMinMax(x - 100, x); } } void Refresh() { m_pCtrl-RefreshCtrl(); } private: COLORREF GetChannelColor(int index) { static const COLORREF colors[] { RGB(255, 0, 0), RGB(0, 255, 0), RGB(0, 0, 255), RGB(255, 255, 0), RGB(255, 0, 255), RGB(0, 255, 255) }; return colors[index % 6]; } CChartCtrl* m_pCtrl nullptr; CChartCtrl* m_pChart nullptr; std::vectorCChartLineSerie* m_series; };这个封装把通道管理和颜色分配都收进去了界面代码只需要在初始化时调Init在数据到达时调AddData在定时器里调Refresh。改造时注意一点AddData里的点数上限和坐标轴范围要匹配否则滚动效果会不自然。另一个改造方向是加交互功能比如鼠标滚轮缩放、拖拽平移、点击显示数值。ChartCtrl 本身支持一些交互但默认可能没开。你可以在对话框里处理鼠标消息根据鼠标位置计算坐标轴的新范围然后调SetMinMax并刷新。这个功能做出来之后波形工具的可玩性会高很多。验证改造是否成功最直接的方法就是跑一段时间看波形是否稳定滚动、内存是否持续增长、CPU 占用是否平稳。我一般会挂一个性能监视器观察 GDI 对象数和内存占用如果这两个指标在几分钟内持续上升说明有资源没释放得回头检查曲线点数和控件刷新逻辑。从那以后我每次集成新的图表控件都会先写一个最小刷新循环跑十分钟确认没有内存泄漏和句柄泄漏再往正式项目里搬。希望帮到你。本文还有配套的精品资源点击获取