简介:面向VC/MFC开发者的MSChart图表绘制实例,演示如何利用微软ActiveX控件绘制柱状图、折线图和饼图。资源内含完整MFC工程,113个文件以头文件(52个h)与源文件(51个cpp)为主,辅以位图、图标、资源脚本及工程配置,压缩包仅82KB,体积小巧却覆盖了MSChart控件在VC中的核心调用方式。目前已有554人学习下载。通过该实例可掌握在VC++中嵌入MSChart控件、绑定数据并输出多种统计图表的方法,尤其适合在VB示例众多而VC示例稀缺的场景下参考,帮助快速解决报表打印和图形化展示需求。
1. 背景与核心需求拆解
VC++的老牌开发者或刚接触桌面应用数据可视化的朋友应该都遇到过这样一个场景:程序里攒了一堆统计数据,总不能用文本框和ListBox硬生生摆上去吧。不管是工业上位机里的实时曲线、内部工具的性能报表,还是课程设计里的成绩分析,图形化展示基本属于刚需。
提到VC++环境下的图表解决方案,很多人第一反应是找第三方库,比如TeeChart、ChartDirector、商业版的Xtreme Chart Pro,或者干脆用纯GDI自己画,网格、坐标轴、图例全部手工实现。但这里我想说的是微软早期就集成在Windows平台里的MSChart控件(Microsoft Chart Control),它在VC++(特别是MFC工程)里用起来非常顺手。这个控件不需要额外的授权费用,随Visual Studio 6.0到Visual Studio 2010左右的标准安装就能用,在老项目的维护和新增功能上有着独特的优势。
这个项目的标题很直白:利用MSChart输出柱状图、折线图、饼图实例。拆开来看,核心需求有三个层次:
- 在MFC对话框程序中嵌入MSChart控件,完成初始化与数据加载;
- 针对三种常用展示场景,掌握柱状图、折线图、饼图的属性配置与数据接口调用;
- 在真实项目里用得上——不只是拖个控件,还要能动态刷新、切换、处理运行时异常。
这三件事看起来是“做个Demo”,实际上你会在过程中踩到不少坑,包括数据格式不匹配、坐标轴不显示、控件在Release版本下崩溃等。这篇文章就专门把这些经验整理出来,适合正在用VC++做上位机或内部工具、又想快速搞定图表展示的开发同学参考。
2. 环境准备与工程搭建
2.1 选择能正常支持MSChart的开发环境
先说一个容易栽跟头的问题:MSChart毕竟是老控件,VS版本越高,它的可用性反而越低。实测下来,Visual Studio 2010及之前的版本,安装后直接在工具箱的“ActiveX控件”里能找到Microsoft Chart Control 6.0 (OLEDB)。如果你用的是VS2013之后的版本,工具箱里有可能不显示,这时候需要手动添加。
手动添加的方法不复杂:在工具箱空白处右键 -> 选择项 -> COM组件 -> 勾选Microsoft Chart Control 6.0。有些机器上还是会找不到,通常是运行库依赖缺失,装上VC++ Redistributable 2005或2008对应版本即可解决。
我自己的主力开发机是Windows 10 + VS2019,实测MSChart控件在MFC工程中依然可以正常工作,这一点不用担心。它毕竟是ActiveX控件,底层是COM组件,只要系统里注册过,调用逻辑基本跟早期版本一致。不过,如果目标机器是64位Windows,最好保证你的应用程序编译目标是x86(Win32),因为MSChart这个COM控件在64位进程下经常出现莫名奇妙的问题。
注意:如果你的工程已经从VC6升级到VS2019,打开旧对话框资源后,MSChart控件仍然会显示为最早的外观样式,这是正常的,不影响数据输出功能。
2.2 在MFC对话框中嵌入MSChart
在工程里新建一个基于对话框的MFC应用后,我们需要做四件事:
- 在资源编辑器中拖入MSChart控件,调整大小并设置ID,例如IDC_MSCHART;
- 为控件添加关联变量。VS2010及更早版本直接右键 -> Add Variable,VS2015之后版本需要手动在类向导中操作;
- 添加MSChart控件的头文件引用,通常是
#include "mschart.h"; - 在对话框的OnInitialDialog中调用初始化函数,完成图表类型、标题、数据列的设置。
如果你在添加变量时提示找不到CMSChart这个类,不用慌,可以手动在工程中添加一个包装类,基类选择CWnd,然后调用CreateControl来动态创建MSChart控件。这种方式对动态创建图形界面的场景特别有用。
// 动态创建MSChart控件的示例代码 // 假设m_chart是CWnd类型成员变量,IDC_MSCHART为控件ID BOOL CMyDialog::OnInitDialog() { CDialogEx::OnInitDialog(); CRect rect; GetDlgItem(IDC_MSCHART)->GetWindowRect(&rect); ScreenToClient(&rect); GetDlgItem(IDC_MSCHART)->DestroyWindow(); m_chart.CreateControl( _T("MSChartLib.MSChart.2"), // ProgID NULL, WS_CHILD | WS_VISIBLE, rect, this, IDC_MSCHART); return TRUE; }这段代码的精髓在于:先删除对话框模板里已经存在的控件,再通过CreateControl动态创建,以确保控件被正确初始化和绑定到CWnd对象上。实际开发中,动态创建比关联变量更方便做条件判断和参数修改,推荐优先使用。
2.3 初始化MSChart对象
MSChart的操作逻辑其实可以用一句话概括:它是一个数据库风格的数据表格控件,图表本身是数据的可视化呈现。因此,初始化时你要先定义“数据表有多少列、每列字段名是什么”,然后逐行灌入数据。
void CMyDialog::InitChart() { // 设图表类型为2D柱状图 m_chart.SetChartType(2); // 设置行数和列数 m_chart.SetRowCount(1); m_chart.SetColumnCount(5); // 设置行标签(这里只有一行) m_chart.SetRowLabel(1, _T("数据组")); // 设置列标签 CString colNames[5] = {_T("一月"), _T("二月"), _T("三月"), _T("四月"), _T("五月")}; for (int i = 1; i <= 5; i++) { m_chart.SetColumnLabel(i, colNames[i-1]); } // 关闭图例,因为行数少的时候图例没什么意义 m_chart.SetShowLegend(FALSE); }这里要注意:MSChart的行列索引都是从1开始的,不是0。很多人第一次用的时候按C/C++的习惯从0开始,结果第一行第一列总是没有数据,排查半天才发现是索引写错了。此外,SetChartType的参数是Variant类型,直接传整数2代表柱状图。常用的图表类型包括:1(3D柱状图)、2(2D柱状图)、3(3D折线图)、4(2D折线图)、5(3D面积图)、6(2D面积图)、14(2D饼图)、15(3D饼图)。
3. 三种图表的核心实现
3.1 柱状图:最直观的数据对比
柱状图的实现难度不大,核心是把数据塞对位置。在上一节初始化基础上,我们要做的工作就是填入数据。
void CMyDialog::SetBarData() { m_chart.SetChartType(2); double data[5] = {85.5, 92.3, 88.7, 94.1, 91.6}; for (int col = 1; col <= 5; col++) { m_chart.SetData(1, col, data[col-1]); } // 设置坐标轴标题 COleVariant vtAxisTitle(_T("成绩值")); m_chart.GetPlot().GetAxis(0, 0).GetAxisTitle().SetText(vtAxisTitle); // 刷新显示 m_chart.Refresh(); }SetData的参数分别是行、列、值,值类型是VARIANT,直接传递double类型会被编译器自动转换。关于GetAxis(0,0),第一个参数是轴索引,0表示Y轴(值轴),1表示X轴(分类轴)。第二个参数在2D图表下填0即可。
柱状图这里有一个非常影响观感的小设置:柱子宽度和间距。默认情况下,MSChart的柱宽可能偏细,多组数据对比时很难看。可以通过GetPlot().GetSeriesCollection()调整每个序列的宽度?实测不必这么麻烦,直接修改控件的ColumnWidth属性可以控制整体柱宽,但前提是图表类型支持。我通常的做法是,设置为“按比例分配”模式:
// 设置柱宽模式 m_chart.GetPlot().GetSeriesCollection().GetItem(1).GetDataPoints().GetItem(1).SetDataPoint(1, 1); // 不推荐使用实际上MSChart对柱宽的控制比较有限,比起死磕这些细节,不如调整控件整体大小和行列比例来获得更好的视觉效果。比如数据点少时,适当调大m_chart控件宽度,柱子自然会显得更饱满。
3.2 折线图:趋势变化一目了然
折线图适合展示随时间或顺序变化的数据趋势,在上位机里最常见的就是温度、压力、速度等参数的实时曲线。MSChart的折线图实现思路和柱状图几乎一致,替换图表类型并填充数据即可。
void CMyDialog::SetLineData() { m_chart.SetChartType(4); // 2D折线图 m_chart.SetRowCount(1); m_chart.SetColumnCount(6); CString str; // 加载动态数据,假设来自某个传感器采集数组 double temps[6] = {23.5, 24.1, 24.8, 25.2, 24.9, 25.6}; for (int i = 1; i <= 6; i++) { m_chart.SetData(1, i, temps[i-1]); str.Format(_T("T%d"), i); m_chart.SetColumnLabel(i, str); } m_chart.GetPlot().GetAxis(0, 0).GetAxisTitle().SetText(COleVariant(_T("温度(°C)"))); m_chart.GetPlot().GetAxis(1, 0).GetAxisTitle().SetText(COleVariant(_T("采样点"))); m_chart.Refresh(); }折线图本身没有太多花哨的操作,但有两个细节值得注意。
第一,坐标轴范围。MSChart默认会自动计算坐标轴的上下限,数据变化不明显时,折线会化身一条直线,看不出波动的区别。这种情况下,需要手动设置坐标轴范围,把波动“放大”。
// 强制设置Y轴范围 m_chart.GetPlot().GetAxis(0, 0).GetValueScale().SetAuto(FALSE); m_chart.GetPlot().GetAxis(0, 0).GetValueScale().SetMinimum(20.0); m_chart.GetPlot().GetAxis(0, 0).GetValueScale().SetMaximum(30.0);第二,序列点的标记样式。数据点上的菱形或圆形标记,能让折线上的每个数值一目了然。默认情况下不一定显示,可以这样设置:
m_chart.GetPlot().GetSeriesCollection().GetItem(1).GetDataPoints().GetItem(1).GetDataPointLabel().SetShow(TRUE);这句代码是给每个数据点显示值标签,等效于在属性页里勾选“显示数据标签”。如果觉得所有点都显示数值过于拥挤,也可以在后期用循环只显示特定点,但这对MSChart来说实现略复杂,不如保持简单方案。
3.3 饼图:百分比占比展示
饼图是三种图表里最受非技术同事欢迎的,因为它直观、好看,适合展示“各组成部分在整体中的占比”。MSChart的饼图数据结构略有不同:饼图本质上是单序列多数据点的展示,不需要定义列标签作为分类轴,每个数据点的Label直接就是分类名称。
void CMyDialog::SetPieData() { m_chart.SetChartType(14); // 2D饼图 m_chart.SetRowCount(1); m_chart.SetColumnCount(4); COleVariant vtLabels[4] = {_T("研发部"), _T("市场部"), _T("售后部"), _T("行政部")}; double vals[4] = {46.0, 28.0, 18.0, 8.0}; for (int i = 1; i <= 4; i++) { m_chart.SetData(1, i, vals[i]); m_chart.SetColumnLabel(i, vtLabels[i-1].bstrVal); } // 设置百分数标签 m_chart.GetPlot().GetSeriesCollection().GetItem(1).GetDataPoints().GetItem(1).GetDataPointLabel().SetShow(TRUE); m_chart.GetPlot().GetSeriesCollection().GetItem(1).GetDataPoints().GetItem(1).GetDataPointLabel().SetLocation(1); // 1表示外侧 m_chart.Refresh(); }饼图要特别注意图例的显示。默认情况下,图例会显示每个数据点对应的列标签,这在饼图里重复且占据空间。建议把图例关掉,直接在扇区外显示百分比或名称,效果更清爽。
如果需要显示“百分比”,MSChart内置的标签格式支持#PERCENT占位符。实测在MFC环境下设置百分比格式的代码稍显繁琐,但可以通过属性页提前设置,或者使用SetDataPointLabel+GetLabelFormat等方式。在实际项目中,我更倾向于先设置数据为具体数值,然后在列标签里拼好“名称+百分比”,省去额外格式化操作。
3.4 三种图表动态切换的通用封装
实际项目里,不太可能一个程序只展示一种图表,更多场景是用户通过下拉框或按钮切换图表类型。这时候可以用一个统一的刷新函数来管理。
void CMyDialog::UpdateChartByType(int nType) { // nType: 2=柱状, 4=折线, 14=饼图 m_chart.SetChartType(nType); m_chart.SetRowCount(1); m_chart.SetColumnCount(5); m_chart.SetColumnLabel(1, _T("类别A")); m_chart.SetColumnLabel(2, _T("类别B")); m_chart.SetColumnLabel(3, _T("类别C")); m_chart.SetColumnLabel(4, _T("类别D")); m_chart.SetColumnLabel(5, _T("类别E")); // 同样的数据,三种图表共用 m_chart.SetData(1, 1, 12.3); m_chart.SetData(1, 2, 23.4); m_chart.SetData(1, 3, 34.5); m_chart.SetData(1, 4, 45.6); m_chart.SetData(1, 5, 56.7); m_chart.Refresh(); }这种封装方式的好处是数据只需要维护一份,切换时图表类型立即生效。不过你可能会发现一个问题:饼图在多行多列数据时显示的是第一部分数据,而柱状图/折线图会显示所有行列的组合。要规避这个问题,建议在切换图表类型时先清空所有行,只设置单行数据,饼图展示会稳定很多。
4. 实战过程中的细节与避坑指南
4.1 数据刷新与性能问题
上位机场景中,图表数据是动态变化的,比如每500毫秒采集一次温度并实时绘制曲线。MSChart的刷新效率不算高,频繁调用Refresh()或SetData会造成界面明显卡顿。
我的做法是:采集15~30个点后一次性更新,或者用定时器控制刷新频率不超过2Hz(每500毫秒一次)。如果对实时性要求更高,建议把MSChart放在独立线程中更新数据,UI线程负责显示。但MSChart本身是ActiveX控件,跨线程访问容易崩溃,稳妥方案是保留UI线程刷新,采集线程只负责缓存数据,用PostMessage通知UI更新。
代码结构上可以这样设计:
// 采集线程回调 VOID CALLBACK CollectProc(HWND hWnd, UINT uMsg, ...) { double sample = GetSensorData(); g_datas.Add(sample); if (g_datas.GetCount() >= 30) { ::PostMessage(hWnd, WM_UPDATE_CHART, 0, 0); } }这样可以把MSChart操作局限在UI线程,避免COM控件跨线程带来的不可预知问题。
4.2 坐标轴标签重叠问题
数据点比较多或标签文字较长时,X轴下方的标签会重叠在一起,显示效果一团糟。这个问题的根因是MSChart的默认坐标轴标签绘制逻辑比较简单,逐步缩放有限。
规避手段有两个:
- 减少数据点数量,聚合显示。比如展示30天数据时,改为按周聚合为5个点;
- 把X轴标签旋转一定角度。MSChart的轴标签旋转角可以通过
GetAxis(1, 0).GetLabel().SetAngle()设置,实测设置为30度或45度效果较好。
m_chart.GetPlot().GetAxis(1, 0).GetLabel().SetAngle(45);这个设置配合适当缩放控件宽度,基本能解决绝大多数标签拥挤问题。
4.3 控件外观与背景色调整
MSChart默认的灰色背景放到现代UI里稍显突兀,至少我在实际交付的自研工具里都会把背景改成白色或浅色系。设置方法比较直接:
// 设置背景为白色 m_chart.GetBackdrop().GetFill().SetStyle(1); // 1表示Solid填充 m_chart.GetBackdrop().GetFill().GetBrush().GetFillColor().Set(0xFFFFFF); // 白色如果你想进一步去掉图表区的灰色边框,可以通过GetPlot().GetBackdrop()做同样操作。这套设置的底层逻辑是COM组件的FillStyle、Brush、FillColor三层属性结构,第一次接触容易晕,但把代码写下来复用一轮之后就熟了。
4.4 常见编译与运行错误速查表
这里整理一份我在实际开发中遇到的错误和解决方案,方便大家直接对照排查:
| 现象 | 原因 | 解决方案 |
|---|---|---|
| Debug编译通过,Release运行崩溃 | MSChart COM控件未在目标机器注册 | 目标机安装VC++ Redistributable或regsvr32 MSChart.ocx |
| 控件拖入对话框后无反应 / 显示空白 | 控件未正确初始化,或使用了64位编译目标 | 确认工程为x86编译;确认CreateControl成功返回 |
| SetData传入字符串或NULL时崩溃 | VARIANT类型不匹配 | 统一使用double或int类型数据 |
| 饼图只显示一块或没显示 | 行数太多或未设置为单行 | 设置RowCount=1后重试 |
| 坐标轴标题不显示 | 未设置轴Title的可见性 | GetAxisTitle().SetShow(TRUE) |
| 图例位置不合适或遮挡图表 | 默认图例自动排布问题 | 使用SetShowLegend(FALSE)关闭或手动定位 |
4.5 动态修改MSChart尺寸时的布局自适应
程序运行时,用户可能会拉伸窗口,MSChart控件默认不会自动缩放。在对话框的OnSize事件中,可以按比例调整控件位置和大小:
void CMyDialog::OnSize(UINT nType, int cx, int cy) { CDialogEx::OnSize(nType, cx, cy); if (m_chart.GetSafeHwnd()) { CRect rc(10, 10, cx - 10, cy - 10); m_chart.MoveWindow(&rc); } }这段代码的核心是为控件留出边距,并让它随窗口变化。注意提前判断GetSafeHwnd(),避免控件尚未创建时调用MoveWindow导致断言错误。
5. 实际使用中的心得与扩展思路
MSChart虽然是一个有点年头的控件,但它“免费、无需额外依赖、MFC集成无缝合适”这三个特点,让它在内部工具和工业软件里依然有一席之地。拿我个人维护的一个设备参数监控程序来说,原来用ListBox输出传感器数据,现场调试员反馈“看得眼睛疼”,后来我用MSChart直接改成折线图显示温湿度趋势,改动量不大,反馈却非常好。这就是为什么我不建议在简单需求里动不动就引入几百MB的第三方图形库。
但在使用过程中你也要有清醒认知:MSChart的交互能力有限,比如不支持鼠标悬浮数据提示、缩放选择区域等现代化交互。如果产品需求偏向数据探索型,可能还是要考虑TeeChart或QT的轻量级方案。不过,对于展示型应用(报表、监控大屏、课程设计),MSChart完全够用。
以上这套代码和排查经验,都是我在实际项目里跑通过的,你可以直接拿过去用。遇到问题欢迎留言交流,至少第一时间帮你看看是数据格式问题还是控件初始化问题——这两种情况占了我用过MSChart以来遇到的八成以上问题。
本文还有配套的精品资源,点击获取