
简介一份基于VC与MSChart ActiveX控件的图表绘制源码实例主要面向MFC程序开发者尤其适合在报表打印、数据分析与统计展示模块中需要快速输出柱状图、折线图、饼图的场景。项目演示了MSChart控件在MFC框架下的完整接入方法包括控件初始化、数据绑定、坐标轴设置与图表刷新并针对常见折线图绘制给出了可运行的参考实现柱状图与饼图的切换逻辑也一目了然。值得一提VB中的MSChart示例较多VC领域的同类资源却相对稀缺这份实例能帮助开发者绕过控件封装中的典型问题直接复用核心绘图代码缩短开发周期。压缩包共113个文件主体为52个h头文件和51个cpp源文件涵盖演示工程入口、绘图封装、数据网格等模块代码文件分工较细便于按模块定位阅读另附bmp、ico图标和资源脚本同时包含dsw、dsp、clw等工程文件构成一个可直接编译学习的完整VC工程压缩包为rar格式整体仅82KB结构紧凑便于直接下载对照学习。已有554人学习下载适合具备一定C基础、希望快速上手MSChart的MFC开发者。 最近在维护一个老项目时客户提了个需求把数据库里的统计数据在界面上用图形展示柱状图、折线图项目里习惯叫拆线图、饼图三种形态能切换。打开工程一看里面引用的图表控件是老熟人——MSChart也就是Microsoft Chart Control 6.0 (SP6)。说实话刚接手时我也犯嘀咕这上古控件还能不能在新版VS里正常用但实际做完三个图表后我反而觉得在VC的MFC环境里MSChart依然是做数据可视化最省事的方案不用引第三方库、不用从头实现绘制逻辑、拖一个控件写几十行代码就能出图。这篇就按我真实的实现顺序把MSChart环境准备、核心数据模型、三类图表代码、踩过的坑和后续扩展完整过一遍给正在用VC做界面统计功能的朋友一份能直接抄作业的实录。1. 环境准备控件注册与工程配置1.1 MSChart控件从哪来MSChart全名Microsoft Chart Control 6.0 (SP6)是微软随Visual Studio 6.0发布的ActiveX图表组件。它虽然年代久远但因为接口稳定、部署简单在MFC项目里一直有存量用户。在VS2017、VS2019、VS2022里这个控件依然能正常使用前提是你得先在工具箱里把它请出来。先确认开发环境。新建工程时选择“MFC应用”应用程序类型选“基于对话框”这样最快——热词里有人问VS2017怎么用VC建立Windows窗体程序实际就是走这个模板。工程创建完成后在工具箱空白处点右键选择“选择项”在弹出的对话框里切到“COM组件”页勾选“Microsoft Chart Control 6.0 (SP6)”确定后工具箱里就会出现一个图表图标拖到对话框上即可。如果你用的是VS2022操作路径完全一致不存在兼容性限制。1.2 找不到控件时的手动注册与x86限制如果你在COM组件列表里没找到Microsoft Chart Control多半是MSChart.ocx没有注册。这时需要管理员权限手动注册。64位系统下注意一个细节MSChart.ocx是32位控件注册时需要用32位的regsvr32也就是C:\Windows\SysWOW64\regsvr32.exe注册完成后组件列表里才能正常显示。很多人第一次注册失败就是因为默认调用了64位版本的那个regsvr32注册完系统提示成功但VS里依然看不到控件。还有一个影响中途编译和后期发布的关键点MSChart.ocx没有64位版本。也就是说你的MFC工程编译目标必须是x86Win32不能是x64。否则编译能通过运行到创建控件时直接报“未注册类别”或崩溃。我见过一个同事在这上面耗了大半天最后发现就是平台选错。新建工程时把平台选成x86后面就不要随意切。另外MSChart依赖VC运行库。程序发到其他机器时目标机器需要安装对应版本的Visual C Redistributable否则程序会提示缺少dll或无法初始化。我实测过安装VC 2015-2022 x86运行库能覆盖大多数MSChart项目的依赖如果目标机器还有旧项目混用直接把2015到2022的x86、x64都装一遍更省心。网上流传的“VC runtime repair tool”本质上就是批量把系统里残缺的运行库重新补全遇到客户机器缺库严重时用这类工具排查很快。2. 初始化与数据模型列是序列行是分类2.1 控件变量的生成方式拖入控件后在控件上右键添加变量VC会通过ClassWizard自动生成CMSChart类mschart.h/mschart.cpp并在对话框类中绑定一个成员变量。常见的绑定代码在DoDataExchange里void CChartDemoDlg::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); DDX_Control(pDX, IDC_MSCHART1, m_Chart); }如果你不想用对话框模板也可以直接调用Create动态创建但对话框拖控件是最省事的路径初始化也直观。有一点提醒CMSChart类是从CWnd派生的它封装了大量COM接口生成后不要手动去改mschart.h里的定义否则容易破坏整个类的导出结构。2.2 行列模型决定了你怎么写数据用MSChart之前先把它内部的数据表结构搞清楚否则代码会绕很多弯路。MSChart的数据本质是一个二维表Row行是分类Column列是序列。这句话值得仔细琢磨。拿最典型的柱状图举例假设要展示四个季度的销售额季度就是分类所以设置4行如果还想同时展示去年和今年两组数据做对比那么每组数据是一个序列一共2列。数据表长这样行分类列1去年列2今年第一季度100120第二季度88105第三季度156170第四季度96140这个二维关系直接决定了代码怎么写先通过SetColumnCount设置列数用SetRowCount设置行数再用SetColumnLabel给每一列起名字也就是图例显示的文字用SetRowLabel给每一行起名字也就是X轴显示的分类标签最后用SetData按坐标填数。这里的坐标关系就是SetData(行号, 列号, 数值)行号和列号都从1开始。如果只做单序列图表比如只有一个“销售额”那就设置1列即可这种设计在饼图里会特别明显——饼图本质上就是一个序列的多分类占比。2.3 初始化代码的固定顺序MSChart初始化时有一个非常容易被忽略的约束必须先设置行列数再设置标签和数据。因为每次改变RowCount或ColumnCount控件内部的数据表都会被重建之前写入的数据和标签全部清空。我把这个顺序固化成一套模板第一步SetChartType指定图表类型第二步SetColumnCount和SetRowCount定义数据表尺寸第三步SetColumnLabel和SetRowLabel设置图例和分类名第四步SetData逐格填充数据按这个顺序来基本不会遇到“数据怎么都不显示”的问题。后面踩坑实录里我会再展开讲如果你发现图表一片空白先检查是不是把SetData写在了SetRowCount前面。3. 三种图表的接口调用与完整代码3.1 柱状图分类对比的标准写法柱状图适合做分类对比比如各部门销售额、各季度出货量。在MSChart里设置图表类型为2D柱状图使用常量值1。void CChartDemoDlg::ShowBarChart() { m_Chart.SetChartType(1); // chBar2d m_Chart.SetColumnCount(1); m_Chart.SetRowCount(4); m_Chart.SetColumnLabel(1, _T(销售额)); m_Chart.SetRowLabel(1, _T(第一季度)); m_Chart.SetRowLabel(2, _T(第二季度)); m_Chart.SetRowLabel(3, _T(第三季度)); m_Chart.SetRowLabel(4, _T(第四季度)); m_Chart.SetData(COleVariant((short)1), COleVariant((short)1), COleVariant(120.5)); m_Chart.SetData(COleVariant((short)2), COleVariant((short)1), COleVariant(88.3)); m_Chart.SetData(COleVariant((short)3), COleVariant((short)1), COleVariant(156.2)); m_Chart.SetData(COleVariant((short)4), COleVariant((short)1), COleVariant(96.0)); m_Chart.SetTitleText(_T(2024年度销售额)); }这里有个重要约定SetData前两个参数必须用COleVariant((short)行号)的方式强转。直接用COleVariant(1)传int类型MSChart的COM接口在解析VARIANT时可能报类型不匹配或干脆不写入数据这种问题不报错、只能靠肉眼在界面上发现非常坑。如果你还想做多系列对比把SetColumnCount改成2再增加一个SetColumnLabel(2, _T(去年))然后按数据表里的二维坐标填充第二列的数据即可多系列的图例文字会由MSChart自动生成。3.2 折线图趋势展示与多序列叠加折线图适合连续时间序列的趋势对比。设置图表类型为2D折线图常量值是3。下面这段代码用12个月的数据同时展示去年和今年两组趋势void CChartDemoDlg::ShowLineChart() { m_Chart.SetChartType(3); // ch2dLine m_Chart.SetColumnCount(2); m_Chart.SetRowCount(12); m_Chart.SetColumnLabel(1, _T(去年)); m_Chart.SetColumnLabel(2, _T(今年)); CString monthName; for (int i 1; i 12; i) { monthName.Format(_T(%d月), i); m_Chart.SetRowLabel(i, monthName); } double lastYear[12] {10, 12, 15, 18, 16, 20, 22, 25, 24, 28, 30, 27}; double thisYear[12] {11, 14, 13, 19, 21, 23, 26, 29, 27, 33, 35, 31}; for (int i 1; i 12; i) { m_Chart.SetData(COleVariant((short)i), COleVariant((short)1), COleVariant(lastYear[i-1])); m_Chart.SetData(COleVariant((short)i), COleVariant((short)2), COleVariant(thisYear[i-1])); } m_Chart.SetTitleText(_T(近两年月度趋势对比)); }折线图和柱状图的代码结构几乎一模一样只是SetChartType不同。这也侧面说明MSChart的数据模型是统一的不管最终显示成什么形状底层都是那张二维表。实际项目里我会把上面的循环逻辑封装成一个函数传入两个double数组这样换数据源的时候不用动图表代码。顺带说一句很多人初次上手时担心折线图的折点太多会不会卡实测几百个点内MSChart绘制都很流畅没必要过度优化。3.3 饼图占比展示的隐藏约束饼图适合展示占比结构比如市场份额、预算分配。设置类型为2D饼图常量值是14。饼图只需要一列数据每一行代表一个扇区MSChart会自动把数值折算成百分比绘制不用自己计算比例这个自动折算逻辑省了不少事。void CChartDemoDlg::ShowPieChart() { m_Chart.SetChartType(14); // ch2dPie m_Chart.SetColumnCount(1); m_Chart.SetRowCount(5); m_Chart.SetColumnLabel(1, _T(市场份额)); m_Chart.SetRowLabel(1, _T(产品A)); m_Chart.SetRowLabel(2, _T(产品B)); m_Chart.SetRowLabel(3, _T(产品C)); m_Chart.SetRowLabel(4, _T(产品D)); m_Chart.SetRowLabel(5, _T(产品E)); m_Chart.SetData(COleVariant((short)1), COleVariant((short)1), COleVariant(30.5)); m_Chart.SetData(COleVariant((short)2), COleVariant((short)1), COleVariant(45.2)); m_Chart.SetData(COleVariant((short)3), COleVariant((short)1), COleVariant(12.0)); m_Chart.SetData(COleVariant((short)4), COleVariant((short)1), COleVariant(8.3)); m_Chart.SetData(COleVariant((short)5), COleVariant((short)1), COleVariant(4.0)); m_Chart.SetTitleText(_T(产品线市场份额分布)); }饼图有两个容易踩的细节。第一ColumnCount必须保持为1如果你手滑设置成2列MSChart虽然不会报错但绘制出来的饼图数据归属会混乱。第二饼图的RowLabel显示位置可能比较靠近圆心或者被截断不同版本表现不一样实在显示不全时可以通过设置字体大小或者调整控件尺寸来缓解。想做3D饼图效果的话把类型改成常量值15即可但3D模式下标签的排布会更紧凑字体设置就更重要了。4. 踩坑实录装上之后最容易翻车的四个场景4.1 图表空白什么图形都没有这是我接手项目时遇到的最典型问题。现象界面上MSChart控件区域一片空白没有坐标轴、没有柱子但程序不报错。排查链路是这样的先把代码整体过一遍看初始化顺序。MSChart在SetRowCount或SetColumnCount时会重建内部数据表意味着之前的SetData和标签设置全会清空。如果你把SetData写在SetRowCount前面数据就被清掉了。正确顺序必须是设置图表类型、设置行列数、设置标签、填充数据。再检查一个隐蔽点RowCount或ColumnCount是不是设置成了0。比如饼图代码有人会觉得“饼图没有分类轴”于是省略了SetRowCount结果就是图上空白。MSChart内部对行列数量有严格要求行数和列数都必须至少为1。还有一版我调试时遇到的情况是把SetData的数值全部传成了0MSChart绘制时不认为这是有效数据表现也是空白。给饼图填充数据前可以先手动打印一下数组内容确认数据源不是全0。4.2 SetData的数据类型不匹配SetData的完整签名要求前两个参数是VARIANT类型而且内部是shortVT_I2第三个参数是数值类型通常用doubleVT_R8。我在代码里写的是COleVariant((short)1)有人会写成COleVariant(1)这在编译期完全没问题但运行期MSChart解析VARIANT时发现类型不符数据写入失败。最迷惑的是它不弹错误框只有图形不更新调试起来特别烦。我的习惯是封装一个辅助函数把行号、列号、数值统一包装好再调用void CChartDemoDlg::SetChartData(int row, int col, double value) { m_Chart.SetData( COleVariant((short)row), COleVariant((short)col), COleVariant(value)); }这样所有调用点只需要关注逻辑参数不需要每处都注意类型强转。如果你的工程使用了ATL的CComVariant一样要保证是VT_I2和VT_R8类型原理相同。4.3 中文标签显示成乱码或问号中文乱码的问题大多数和字符集设置有关。新建MFC工程时推荐使用Unicode字符集传字符串时用_T()或者L前缀SetRowLabel和SetColumnLabel都按这个来一般不会乱码。如果还是无法显示可以尝试给MSChart的字体指定中文字体。CMSChart类封装的接口里有GetFont方法m_Chart.GetFont().SetName(_T(宋体)); m_Chart.GetFont().SetSize(9);这个设置要在填充数据之前做否则绘制完成后字体改动不会立即刷新。另外控件尺寸太小也会让中文标签被截断这种不是乱码而是“显示不全”建议把控件拉大一些或者减少分类行数别指望MSChart自动换行。4.4 发布到别的机器上无法启动程序在自己电脑上一切正常打包发给客户后客户一打开就弹“未注册类别”或者干脆启动崩溃。这个问题我排查过好几次根子在于MSChart控件和VC运行库没有随程序分发。MSChart作为ActiveX控件目标机器上必须能创建这个COM组件。最稳妥的方式是在安装包里附带MSChart.ocx安装时执行regsvr32注册注意64位系统下用C:\Windows\SysWOW64\regsvr32.exe这个32位版本注册。如果注册成功还是崩下一步检查运行库MSChart依赖MSVC运行库至少要安装VC 2015-2022 x86版。很多“VC runtime repair tool”类工具就是用来批量修复这类运行库缺失问题的客户机器上如果缺得厉害直接用工具扫描修复一次往往就解决了。部署这块最容易忽略的是把运行库做成静默安装包装到安装包里一次搞定别指望客户手动装。5. 从Demo到可用动态刷新与事件交互的进阶思路5.1 定时刷新图表数据实际项目中图表数据很少是写死在代码里的更多是从数据库或串口设备实时读取。我常用的做法是在对话框OnInitDialog里启动一个定时器在OnTimer里重新查询数据并填充图表。void CChartDemoDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1) { RefreshChartData(); } CDialogEx::OnTimer(nIDEvent); } void CChartDemoDlg::RefreshChartData() { // 从数据库获取最新数据填充到数组之后复用前面的ShowLineChart逻辑 ShowLineChart(); }要注意一点MSChart控件在改变行列数之后会重建内部表如果你只是刷新数值行数列数不变直接重复SetData即可不需要重新设置ChartType和标签这样刷新速度更快也不会闪烁。反过来如果数据源结构变了比如分类数量变了就必须把行列数一并更新。实测下来MSChart对短时间内频繁重绘的支撑够用几百毫秒刷一次曲线没有明显卡顿如果数据量大到几千行建议降低刷新频率或者在内存里攒一批数据再一次性SetData否则界面会明显掉帧。5.2 点击图表实现联动交互如果你做过Web端图表肯定熟悉ECharts的鼠标悬浮提示、点击高亮这些交互。MSChart作为老控件交互能力没有那么丰富但也不至于完全不可用。MSChart控件提供了Click、OnSeriesSelected等事件通过MFC的事件接收机制可以捕获。最常用的场景是“鼠标点了哪一列下方文本区显示对应数据”在对话框上给MSChart添加事件处理函数类向导里找到MSChart的事件列表选择Click然后在处理函数里通过GetRow和GetColumn获取当前点击的行列位置再去查询详细数据。void CChartDemoDlg::OnClickMsChart() { short row m_Chart.GetRow(); short col m_Chart.GetColumn(); CString strInfo; strInfo.Format(_T(当前选中第%d行第%d列), row, col); SetDlgItemText(IDC_EDIT_INFO, strInfo); }这里GetRow和GetColumn返回的是MSChart内部数据表的行列坐标不是像素坐标。如果你只是想在鼠标所在位置显示浮动数值MSChart原生并没有直接接口需要自己处理鼠标Move和坐标换算那就要结合GetPlotRect等接口做坐标映射了。这个属于高阶玩法市面上也有很多人直接用第三方控件或者封装好的MFC图表类替代但如果你只想在现有MSChart基础上做轻量联动事件处理就够了。5.3 用同一套方法封装三种图表切换最后分享一个我自己的工程习惯把前面的功能封装成一个方法用枚举值控制图表类型。界面三个按钮分别调用同一个方法内部判断类型走不同逻辑void CChartDemoDlg::UpdateChart(int chartType) { switch (chartType) { case 1: ShowBarChart(); break; case 2: ShowLineChart(); break; case 3: ShowPieChart(); break; } }这个封装的收益在做完三个图表后特别明显。客户如果临时要求换配色、加图例、调标题所有改动都集中在对应的ShowXxxChart函数里不会散落在各个按钮事件中。我在多个项目里都用这个套路别人接手读代码也直观。顺带说一句如果你后续要支持3D效果MSChart还提供了三维柱状图和三维饼图类型只需要改一下SetChartType的数值这个扩展空间也够日常使用了。本文还有配套的精品资源点击获取