
简介ChartCtrl 是一套基于 MFC 的老牌图表控件源码已完成 VS2015 工程适配可导入现代开发环境直接编译使用面向需要在 Windows 桌面程序中快速加入数据可视化图表的 C 开发者尤其适合希望直接复用成熟控件而不是从零绘制的场景。资源共包含 86 个文件以 41 个 h 头文件和 32 个 cpp 源文件为主体h 文件负责类接口声明cpp 文件实现具体绘图逻辑另有少量 inl 内联模板、工程配置文件及图标资源压缩包整体仅 154KB轻量紧凑。控件封装了曲线、柱状、甘特、表面、蜡烛等多种图表系列并配套坐标轴、网格、图例、十字光标、滚动条等交互组件通过自带 Demo 可了解各模块的组织方式方便移植与二次开发。已有 1005 人学习下载对于学习和研究图表控件设计是一个不错的参考可在此基础上继续优化外观和扩展功能。 做上位机开发或者数据采集相关工作的朋友大概率都见过或者听过 ChartCtrl 这个名字。这是个非常老牌的 MFC 图表控件最早出现在 CodeProject 上用来画实时曲线、趋势图、柱状图在那个还没有 Qt 遍地走的年代几乎就是 MFC 工程师的标配。我最近拿到一份从老工程里翻出来的 ChartCtrl 源码花了两天时间把它适配到 VS2015 环境下过程中踩了不少编译和字符集相关的坑索性把整个移植过程、源码结构、以及实际画曲线的用法一起整理成这篇笔记希望对还在维护 MFC 老项目或者想快速画点图的朋友有帮助。本文适合这几类读者看手头有老 MFC 工程需要迁移到 VS2015 的需要在对话框里快速实现实时数据曲线的以及自己写绘图代码觉得麻烦、想找个轻量控件二次开发的。我会把优化点、工程配置、核心接口和常见报错都写清楚。1. 一个快二十年的老控件凭什么到现在还有人用1.1 这个控件到底能干什么ChartCtrl 核心就是一个窗口类负责把一系列数据点画成可视化的折线图。它不像现在的前端图表库那样自带一大堆交互动画而是老老实实做一件事把数据映射到屏幕坐标然后画线。但它能做到的事情其实不少我梳理一下常见的用法多条曲线同时显示每条曲线单独设置颜色、线宽、点样式自定义 X 轴和 Y 轴范围支持自动根据数据范围调整或者手动固定轴标题、图表标题、网格线、刻度文字都可以设置支持折线系列、散点系列有的版本还有柱状系列支持关闭自动刷新在批量添加数据时手动控制重绘避免界面闪烁。在实时监控类软件里典型场景就是串口或者网口不断收到传感器数据界面左边一个 ChartCtrl 窗口右边一个列表数据一来曲线就跟着动。这种场景下 ChartCtrl 比大部分商业控件更合适因为它足够轻、足够直接也没有第三方依赖拷贝几个源文件就能用。1.2 为什么不直接用新图表库现在做桌面端图表很多人第一反应是用 QtCharts、QCustomPlot 或者 C# 里的 LiveChartMFC 配上 ChartCtrl 是不是有点过时了这个问题的答案取决于项目背景。如果是全新项目我也更推荐考虑跨平台方案但如果是维护老工程把一个已经跑了好几年的 MFC 程序迁移到 Qt成本和风险远比把 ChartCtrl 适配到新编译器高得多。不同方案的对比大致是这样的方案优点缺点适用场景ChartCtrl源码开放、轻量、无版权费用功能基础、交互能力弱老 MFC 项目、快速原型、嵌入式上位机自绘窗口完全可控、可做复杂交互开发量大、需要自己处理刷新有特殊视觉或交互需求QtCharts / QCustomPlot跨平台、功能全、颜值高需要引入 Qt 框架新项目、跨平台桌面端商业控件TeeChart 等功能强、有技术支持收费、体积大、授权麻烦企业级报表、对稳定性要求极高说白了ChartCtrl 的定位是“够用就好”。一段几百行的绘图代码完全能看懂出了 bug 能自己定位这在新式框架里反而不容易做到。1.3 源码结构一览一份典型的 ChartCtrl 源码包含下面这些文件ChartCtrl.h / ChartCtrl.cpp主控件类负责窗口绘制、坐标轴管理、背景和网格ChartAxis.h / ChartAxis.cpp坐标轴类管理刻度和标签ChartLineSerie.h / ChartLineSerie.cpp折线数据系列存储数据点并绘制曲线ChartPointSerie.h / ChartPointSerie.cpp散点数据系列ChartString.h / ChartString.cpp文本标签辅助类。核心关系可以这样理解CChartCtrl 是容器持有坐标轴对象和若干个 series 对象每个 series 负责存自己的数据点并在控件 OnPaint 的时候把自己画出来。这种设计拆得比较干净想加一种柱状系列照着 LineSerie 的写法扩展一个类就能做。2. VS2015 环境下移植优化的核心改动2.1 老代码在 VS2015 下的典型编译问题拿到老源码第一件事不是看功能而是编译。这套源码可能早期是拿到 VC6 或者 VS2003 环境下写的和 VS2015 的编译器差距非常大。常见的编译问题集中在以下几类第一类是 C 语法兼容问题。VC6 时代的编译器对标准 C 支持很差比如 for 循环里声明的变量作用域、模板定义、嵌套模板的右移符号老代码里经常出现for(int i0; ...)后面又在循环外用 i 的写法这在 VC6 能过在新编译器直接报错。第二类是字符串和字符集问题。老代码大量使用 char*、LPCTSTR、CString 混用而 VS2015 新建的项目默认使用 Unicode 字符集很多地方要么编译不过要么运行时出现乱码。还有一个常见坑是CString::Format里用%s配 char 指针在 Unicode 编译选项下格式化出来的内容完全不对。第三类是 MFC 版本差异。老代码可能用了一些旧版 MFC 的 API比如GetWindowText的参数类型变化、CString::GetBuffer的使用方式等需要根据 MFC 14.0 的接口重新调整。第四类是 CRT 告警。老代码里这么多 sprintf、strcpy、vsprintf 是躲不掉的,VS2015 对这个管得特别严格直接报 C4996 警告如果项目设置了“警告视为错误”编译就过不去。2.2 关键修改点盘点我这次移植主要改动集中在这几个地方把所有char类型处理改成TCHAR或CString并统一用_T()宏包裹字符串常量修正 for 循环变量作用域把可能在外部引用的循环变量提前声明添加#include tchar.h和必要的 STL 头文件在预处理器中添加_CRT_SECURE_NO_WARNINGS屏蔽 CRT 安全函数告警对CString::Format中所有格式化参数做了一轮类型核对%d对应 int%f对应 double%s必须用LPTSTR对于 OnPaint 里的内存 DC 操作把旧的GetBuffer/ReleaseBuffer初始化方式改成CBitmap::CreateCompatibleBitmap的现代写法。这里最花时间的是字符集统一。我建议不要图省事把工程属性改成“多字节字符集”因为老工程里其他模块可能已经按 Unicode 写好了。正确做法是修改控件源码把所有字符串都走 TCHAR 宏这样在 Unicode 工程里也能正常运行。2.3 工程属性配置源码改完之后使用方的工程配置也要跟上。在 VS2015 中如果是新建一个 MFC 对话框应用程序来集成 ChartCtrl建议按下面几步配置项目属性 - 常规 - “MFC 的使用”选择“在共享 DLL 中使用 MFC”项目属性 - 常规 - “字符集”保持“使用 Unicode 字符集”项目属性 - C/C - 预处理器 - 预处理器定义添加_CRT_SECURE_NO_WARNINGS如果需要静态链接把“MFC 的使用”改成“在静态库中使用 MFC”但要注意老代码对 DLL 版本 MFC 的兼容性最好建议先跑共享 DLL 方式。如果编译时出现大量 LNK2019 或者 LNK2005 错误通常就是 MFC 链接方式不一致造成的。检查一整个解决方案里所有项目确保它们都使用相同方式的 MFC 链接。2.4 搭建最小可运行工程改完源码之后搭一个最小工程验证是很必要的。我习惯步骤是新建一个 MFC 对话框应用把 ChartCtrl 的 cpp 和 h 文件全部拷贝到项目目录并在 VS2015 里添加到项目然后在对话框资源里拖一个 Picture Control 控件给它的 ID 设为IDC_CHART再为控件添加变量变量类型手动改成CChartCtrl别忘了 include ChartCtrl.h。最后在 OnInitDialog 里写几行初始化代码m_chartCtrl.SetTitle(_T(测试曲线)); m_chartCtrl.SetXRange(0, 100); m_chartCtrl.SetYRange(0, 100); CChartLineSerie* pSerie m_chartCtrl.CreateLineSerie(); pSerie-SetColor(RGB(255, 0, 0)); pSerie-SetWidth(2); pSerie-AddPoint(0, 50); pSerie-AddPoint(50, 80); pSerie-AddPoint(100, 20);编译运行如果对话框上能看到一条从 (0,50) 到 (50,80) 再到 (100,20) 的折线说明移植成功了。3. 实战在对话框里画一条实时温度曲线3.1 核心接口逐个讲用 ChartCtrl 之前先把几个出现频率最高的接口搞清楚。首先是CreateLineSerie()它会在控件内部创建一个折线系列返回指针后续所有对该系列的操作都通过这个指针完成。注意不需要手动 delete控件析构的时候会统一清理。然后是SetXRange和SetYRange分别设置显示范围。如果设了自动刷新每次设置范围都会触发重绘。这里有一个新手容易踩的坑如果没设置范围就 AddPoint很多版本默认范围是 0 到 1数据点画出来可能根本不在可视区域。所以首次使用务必先设置范围。AddPoint是往系列里追加数据点参数是 (x, y)。老版本还有AddPoints之类的批量接口但最简单的就是循环调用。SetColor和SetWidth用来设置曲线的外观SetTitle、SetXLabel、SetYLabel用来设置文字。EnableRefresh是性能开关。当你要一次性加入几千个数据点的时候每 Add 一个点都重绘一次界面会卡到没法看。正确做法是先用EnableRefresh(FALSE)关闭刷新加完所有点以后再EnableRefresh(TRUE)手动刷新一次。3.2 完整示例代码下面这段代码是我在对话框程序里跑通的例子模拟温度传感器每分钟上报一次数据曲线实时更新。// 对话框头文件中 #include ChartCtrl.h CChartCtrl m_chartCtrl; CChartLineSerie* m_pTempSerie; int m_sampleCount; // OnInitDialog BOOL CMyDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 基础设置 m_chartCtrl.SetTitle(_T(实时温度曲线)); m_chartCtrl.SetXLabel(_T(时间(min))); m_chartCtrl.SetYLabel(_T(温度(℃))); m_chartCtrl.SetXRange(0, 60); m_chartCtrl.SetYRange(0, 100); // 创建两个系列对比显示 m_pTempSerie m_chartCtrl.CreateLineSerie(); m_pTempSerie-SetColor(RGB(220, 40, 40)); m_pTempSerie-SetWidth(2); CChartLineSerie* pAvgSerie m_chartCtrl.CreateLineSerie(); pAvgSerie-SetColor(RGB(60, 60, 220)); pAvgSerie-SetWidth(1); m_sampleCount 0; // 用定时器模拟每秒来一个数据 SetTimer(1, 1000, NULL); return TRUE; } void CMyDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1) { // 模拟温度读数50度上下正弦波动 double x m_sampleCount; double y 50 20 * sin(x * 0.15) (rand() % 10 - 5); m_pTempSerie-AddPoint(x, y); // 每隔一段时间窗口跟着数据向右平移 double maxX, minX; m_chartCtrl.GetXRange(minX, maxX); if (x maxX) { m_chartCtrl.SetXRange(x - 60, x); } } CDialogEx::OnTimer(nIDEvent); }跑起来以后曲线会从左边开始画到窗口右边界后整体平移形成类似示波器的滚动效果。3.3 实时滚动的实现思路上面代码里有一个关键点窗口平移。很多实时曲线都需要这种“数据一直往右走画布跟着走”的效果。ChartCtrl 没有内置滚动条机制所以需要自己控制 X 轴的显示区间。我的方法是在每次 AddPoint 之后先通过GetXRange拿到当前 X 轴范围判断新增的点是否超出了右边界。如果超出了就调用SetXRange(x - windowSize, x)让窗口右边界跟上新数据点。这里的 windowSize 就是你想在屏幕上显示多少数据宽度。这个方法有两个好处第一X 轴刻度范围和显示内容同步移动曲线视觉上非常平滑第二不会无限累积旧数据因为 ChartCtrl 内部会一直保留所有数据点绘制时只画可视范围内的点数据量太大再加上截断策略也不难。3.4 用户坐标到屏幕坐标的换算逻辑如果以后要自己扩展 ChartCtrl或者想理解它内部为什么会这样工作那坐标换算是绕不开的。ChartCtrl 在绘制每条曲线时会把用户坐标系下的 (x, y) 换算成窗口客户区的像素坐标 (sx, sy)。核心是一个线性映射sx marginLeft (userX - minX) / (maxX - minX) * (clientWidth - marginLeft - marginRight) sy clientHeight - marginBottom - (userY - minY) / (maxY - minY) * (clientHeight - marginTop - marginBottom)也就是先算数据点在当前显示区间内的比例再乘以绘图区域的像素宽度或高度最后加上边距偏移。Y 轴之所以要用“客户区高度减边距再减”是因为屏幕坐标系 Y 轴向下而图表习惯上 Y 轴向上。了解这个公式之后很多问题都能解释通了为什么没设范围时曲线看不见因为 minX0、maxX1而实际数据 x 是几百比例全跑到 1 之外自然画到窗口外面去了。为什么 Y 轴上下颠倒多半是自己修改时把 bottom 方向的符号搞反了。这个细节对二次开发非常有用。4. 常见问题与排障记录4.1 编译错误速查表移植和使用过程中最容易碰到的编译错误和解决思路我整理成了一张表错误现象根本原因解决办法error C2065“i”未声明for 循环变量作用域变化将循环变量提到循环外声明error C4996vsprintf 被弃用使用了非安全 CRT 函数加_CRT_SECURE_NO_WARNINGS或改用安全版本error C2664Format 参数不匹配char* 与 TCHAR* 混用统一使用_T()Format 的%s配 CStringLNK2019无法解析的外部符号MFC 链接方式不一致项目属性中统一“MFC 的使用”配置运行后界面无曲线未设置 X/Y 范围或未调用刷新设置范围后调用EnableRefresh(TRUE)或Invalidate()中文标签乱码字符集不一致统一 Unicode字符串全部使用_T()除了表格里的问题还有一个很隐晦的坑如果工程设置了“使用 Unicode 字符集”但 ChartCtrl 里某个字符串常量直接写成了温度而非_T(温度)编译器会把它当成窄字符串宽字符版本下 TextOut 只能画出乱码或者空白。检查方式很简单全文搜索中文字符把所有直接写在引号里的字符串都加上_T()。4.2 显示与刷新方面的坑图表控件最常见的问题是没有曲线、闪屏、字体模糊。没有曲线的原因刚才已经说过大多数时候是范围问题或者刷新开关没有打开。闪屏问题一般出在高频刷新上。如果定时器每隔 100ms 就 AddPoint 并且自动刷新窗口会很容易闪烁。解决办法有两个方向一是降低刷新频率比如 1 秒刷新一次数据在一个回调周期内攒批添加二是开启双缓冲。新版 ChartCtrl 里已经有双缓冲封装如果没有可以在 OnPaint 里创建内存 DC 和兼容位图统一画完再 BitBlt 到屏幕闪烁问题会立刻缓解。字体模糊的问题通常和不一致的高DPI设置有关。如果你在 VS2015 里运行正常但在高分屏笔记本上运行字体发虚建议在项目清单里声明 PerMonitorV2 DPI 感知或者干脆固定为系统缩放模式会好很多。4.3 性能优化技巧数据量大了以后ChartCtrl 的绘制性能会明显下降尤其是采样频率高、长时间运行的环境。我这里给三个实测有效的优化手段。第一控制刷新频率。不要每个数据点都刷新用定时器累积一批数据统一 AddPoint 后只刷新一次。我测试过一个系列 10 万个数据点的情况下减少重绘次数比优化绘制逻辑效果更直接。第二滑动窗口截断数据。如果只关心最近 60 秒的数据那超过 60 秒之前的数据点没有保留意义。ChartCtrl 的 series 内部是一个 vector数据无限制增长会导致每次绘制全量扫描。建议在每次 AddPoint 前判断数据量是否超过阈值如果超过用类似erase(series.begin())的方法清理头部数据或者整体移动到新的 vector。第三简化网格和刻度。把网格线的间距调大一些或者关掉 X 轴坐标为小数的刻度显示能减少每次重绘时字符串格式化和画线的开销。曲线多的时候优先保证数据线清晰次要的视觉元素能省就省。注意如果在工作线程里频繁调用 AddPoint一定要加锁或者用 PostMessage 把数据丢给 UI 线程处理。ChartCtrl 内部没有做线程安全处理两个线程同时操作数据容器轻则数据错乱重则直接崩溃。5. 一点后续扩展想法ChartCtrl 虽然老结构却很简单稍微改一改就能扩展出不少功能。我自己后面准备做两件事一是给 LineSerie 加一个中值平滑滤波让传感器毛刺看起来不那么尖锐二是加一个“暂停刷新”按钮点击后曲线保持静态同时数据仍然在后台累积便于回看关键时刻的波形。如果你也在折腾这个老控件建议先从最小工程跑通开始再加业务逻辑。一股脑把数据采集、文件存储、网络通信全堆进去出了问题反而不好定位。把控件本身的边界测清楚后面就顺手了。本文还有配套的精品资源点击获取