简介:面向VC/MFC开发者的MSChart图表绘制示例,围绕在Visual C++环境下输出柱状图、折线图与饼图等统计图表的核心需求展开,解决报表打印与数据可视化场景中缺少现成C++参考的问题,适合需要把统计结果嵌入桌面程序、并希望在界面中直观呈现的开发者。压缩包以RAR格式提供,共113个文件,整体约82KB;其中主体为52个头文件与51个C++源文件,覆盖MSChart控件封装、数据网格、数据点标签、序列绘制等关键模块,另含少量位图、图标、资源脚本与工程配置文件,结构清晰,可直接打开编译并对照学习。已有554人学习下载,可作为报表打印及图表显示类应用的入门示例,尤其适用于VC6或Visual Studio环境下的MFC项目。读者可以从完整Demo工程中观察到MSChart控件的集成方式、图表数据的绑定与刷新流程,也可以将柱状图、折线图、饼图等模块当作模板,快速迁移到自己的统计报表、数据监控或分析工具中,节省重复封装与调试时间;示例代码采用模块化组织,便于按需提取。
1. 为什么还在用 MSChart:老控件在 VC++ 项目里依然能打的报表底子
项目标题里的“拆线图”三个字,在需求文档里经常见到,其实就是折线图。MSChart 是 VC++ 的 MFC 工程里最常见的老牌图表控件,做柱状图、折线图、饼图这类统计输出时,它不需要额外买授权,也不像第三方图表库那样要打包一堆 DLL 跟着程序发布。对于维护老系统、做内部工具、搭教学演示的人来说,把 MSChart 用熟,三种图加起来半天就能出活。这篇笔记从控件注册开始,把数据模型的对应关系、图表类型切换参数、样式定制、导出图片和部署时常见的坑都过一遍,新手能照着敲,熟手能当排查清单用。
2. MSChart 控件的加载与初始化:从注册到对话框绑定的完整链路
2.1 控件注册与工具箱加载:环境没配好,一切白搭
MSChart 在 VC++ 里以 ActiveX 控件的形式存在。你新建一个基于对话框的 MFC 工程,打开对话框编辑器,如果控件工具箱里找不到 Microsoft Chart Control,或者拖上去之后显示成一个灰色框,绝大多数情况是控件没有注册到当前系统。它不像那几个标准控件装完系统就有,MSChart 需要有单独的注册信息。
常见的注册做法是在命令行执行:
regsvr32 mschart.ocx如果提示找不到文件,先确认这个控件文件在当前机器上是否存在。老开发机上通常会有,系统是 64 位而控件是 32 位时,要把 regsvr32 换成 SysWOW64 目录下的那个版本,否则注册提示成功,程序却依然找不到控件。注册完成后,在对话框编辑器里右键选择插入 ActiveX 控件,列表里出现 Microsoft Chart Control,就能像放普通控件一样使用它了。
注意:别在项目里打包一个注册脚本让用户手动运行。更稳的做法是在安装程序里做控件注册,或者程序第一次启动时调用 DllRegisterServer,避免用户权限不足导致注册失败。
2.2 在对话框上放置控件并绑定 CMSChart 变量
注册成功之后,把控件拖进对话框。选中图表控件,打开类向导,给它添加一个成员变量,类型自动生成 CMSChart。这个变量是控件的 C++ 包装类,后面所有绘图操作都通过它来完成。
添加完变量后,对话框的 DoDataExchange 里会多一行绑定代码,类似这样:
void CReportDlg::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); DDX_Control(pDX, IDC_MSCHART1, m_Chart); // 控件资源和 C++ 对象关联 }这里的 IDC_MSCHART1 是资源编辑器自动分配的控件 ID。MFC 里就是用 DDX_Control 把控件资源和成员变量绑在一起。如果你看不到这一行,检查是不是把控件当成普通 Custom Control 拖进来的,而不是从 ActiveX 控件列表里插入的,两者在资源类型上完全不同。
绑定成功之后,在 OnInitDialog 里就可以直接调用 m_Chart 的方法。第一次调试时可以先不写任何绘图代码,只运行对话框,如果界面上出现一个空白图表区域,说明控件生命周期正常。这一步跑通,后面的事才有着落。
2.3 用代码动态创建 MSChart:不依赖资源文件的另一种做法
有些场景下,一个对话框上不止一个图表,或者图表要放在 Tab 页里动态出现,这时不想在资源编辑器里堆十几个控件,可以改成代码创建。CMSChart 从 CWnd 派生,可以用 CWnd::CreateControl 来创建这个 ActiveX 控件:
BOOL CReportDlg::CreateChart(CRect rect, UINT nID) { if (m_Chart.GetSafeHwnd() != NULL) return TRUE; return m_Chart.CreateControl( _T("MSChart.Chart"), // ActiveX 控件的 ProgID WS_CHILD | WS_VISIBLE, rect, this, nID); }CreateControl 第一个参数是控件的 ProgID,MSChart 的通用 ProgID 就是 MSChart.Chart,不同版本后面可能带后缀,但通常这个通用 ID 都能解析。后面的参数和 CreateWindow 类似:窗口样式、位置、父窗口、控件 ID。注意 nID 不能和对话框上其他控件重复,否则消息路由和 Tab 顺序都会乱。
动态创建的好处是运行时才能决定位置和大小,比如把客户区拆成左右两块,左边放图表右边放明细表。坏处是控件的属性表对话框没法在资源编辑器里预览,所有样式都要用代码设置。我一般只在图表数量不确定时用动态创建,固定数量的报表还是放资源文件里,省事且直观。
3. 输出柱状图:先摸清数据网格的脾性,再调样式参数
3.1 行、列、系列与分类的对应关系:MSChart 不按 XY 轴思考
MSChart 对数据的理解不是“X 轴一个数组、Y 轴一个数组”,而是一张二维网格,类似 Excel 的一个区域。行和列都从 0 开始编号。对于柱状图:列对应图例里的系列,行对应 X 轴上的分类。比如要画“三个产品线在 1 到 6 月的销量”,行数是 6,因为 6 个月;列数是 3,因为 3 条产品线;每个单元格存一个数值。
这个模型在第一次用的时候特别容易翻车,因为自然直觉是拿一个数组装横轴刻度,另一个数组装数值,结果怎么设置图都是乱的。记住一个顺序:先问自己行是什么、列是什么,写下来,再去调 SetRowCount 和 SetColumnCount。
// 6 行 3 列的数据网格 m_Chart.SetRowCount(6); // 分类数量:6 个月 m_Chart.SetColumnCount(3); // 系列数量:3 个产品线如果只设置行列而不设置标签,X 轴和图例会显示成序号。MSChart 的默认行为是给没有标签的行列显示编号,所以不给标签也能画出图,但交到别人手里的报表必须给标签。数据网格的行列决定结构,标签决定可读性,这两件事在代码里是分开的。
3.2 柱状图最小可运行代码:从空表到一张能看的图
下面是柱状图最常见的一套写法,放在 OnInitDialog 里直接能跑:
// 2D 柱状图 m_Chart.SetChartType(1); // 1 = 二维柱状图,0 = 三维 m_Chart.SetRowCount(6); // 6 个分类:1-6 月 m_Chart.SetColumnCount(3); // 3 个系列:三条产品线 // 列标签是图例文字 m_Chart.SetColumnLabel(0, _T("产品A")); m_Chart.SetColumnLabel(1, _T("产品B")); m_Chart.SetColumnLabel(2, _T("产品C")); // 行标签是 X 轴刻度文字 CString arrMonth[6] = { _T("一月"), _T("二月"), _T("三月"), _T("四月"), _T("五月"), _T("六月") }; for (int r = 0; r < 6; r++) m_Chart.SetRowLabel(r, arrMonth[r]); // 填充数据:行是月份,列是产品线 double vals[6][3] = { { 120, 80, 45 }, { 135, 75, 60 }, { 108, 90, 55 }, { 156, 85, 72 }, { 142, 96, 68 }, { 188, 110, 80 } }; for (int r = 0; r < 6; r++) for (int c = 0; c < 3; c++) m_Chart.SetData(r, c, COleVariant(vals[r][c]));这套代码里有几个顺序不能乱:SetChartType、SetRowCount、SetColumnCount 必须在 SetData 之前调用,否则数据写不进去,或者写进去又因为网格重建被清掉。SetData 的三个参数是行号、列号、数值,数值要用 COleVariant 包一层,因为 ActiveX 属性接收的是 VARIANT 类型。如果发现柱子的颜色全一样或者数据对不上位置,多半是循环里行列号写反了。
3.3 让柱状图能放进正式报表:坐标轴标题、图例位置和网格线
数据能显示之后,样式才是报表能不能交差的重点。MSChart 的样式接口层级很深,Plot 是绘图区,往下还有坐标轴、数据标签和系列集合。不同版本的 MFC 打包类对 Plot 暴露的方式不完全一样,较早的 VC 环境里比较常见的链式调用是这样:
// 坐标轴标题:取 Plot 绘图区,再取 X 轴(0,0)和 Y 轴(0,1) m_Chart.GetPlot()->GetAxis(0, 0)->GetAxisTitle()->SetText(_T("月份")); m_Chart.GetPlot()->GetAxis(0, 1)->GetAxisTitle()->SetText(_T("销量(台)")); // 图例放到右下角 m_Chart.GetLegend()->SetLocation(4); // 隐藏 Y 轴网格线,让柱子更干净 m_Chart.GetPlot()->GetAxis(0, 1)->GetAxisGrid()->SetMajorLineStyle(0);这里说明三点。第一,GetPlot 返回的是绘图区对象,GetAxis 第一个参数固定传 0,第二个参数 0 是 X 轴、1 是 Y 轴。第二,这些链式调用要在 SetChartType 之后再执行,因为图表类型会改变坐标轴对象的状态。第三,不同版本对这套接口的命名不完全一致,编译报错时去工程里自动生成的 mschart.h 里查实际方法名,以本机头文件为准。
我一般把这三段放在一个独立的 SetupStyle() 函数里,和填数据的逻辑分开。这样柱状图、折线图切换时,只需要改图表类型和重新填数据,样式代码不用动。
4. 折线图与饼图:复用同一个数据网格,只换几个参数
4.1 折线图:切换 ChartType,调整数据点的呈现方式
折线图对数据网格的解读方式和柱状图完全一样,行是分类,列是系列。所以前面柱状图的 SetRowCount、SetColumnCount、SetData 都可以原样保留,唯一必须改的是图表类型:
m_Chart.SetChartType(3); // 3 = 二维折线图如果你之前设过 6 行 3 列,这一行代码就够完成从柱状图到折线图的切换。柱状图的列标签、行标签、数值全部复用。这个特性在实际项目里非常实用,意味着你可以把绘图逻辑做成一个按类型分派的分支,而不是每个图各写一遍。
折线图和柱状图真正不同的地方在视觉细节。柱状图靠柱子高度表达数据,折线图靠折线和数据点位置表达数据。当数据点不多时,建议把点样式打开,否则一条光秃秃的线看不出具体数值。另一个常见问题是折线图在数据点多且窗口窄的时候会显得锯齿明显,这时候不要急着换插值算法,先把 Y 轴网格线打开、把绘图区拉大,视觉上会好很多。
如果甲方要求平滑曲线,MSChart 本身不做样条插值,我一般会在填数据前把数据点做一次手工插值再喂进去,比如相邻两点中间按时间比例补一个点。这是折线图项目里最容易被问到的需求,但先确认是“要平滑曲线”还是“要趋势线”,别一上来就插值。
4.2 饼图:一维数据网格、扇区标签与图例的特别处理
饼图是三类图里最特殊的,它把数据网格当一行来读。行数必须设为 1,列数等于扇区数,列标签就是扇区名。
// 2D 饼图,枚举值常见为 14,以本机 mschart.h 里 VtChChartType2dPie 为准 m_Chart.SetChartType(14); m_Chart.SetRowCount(1); // 饼图只有一行 m_Chart.SetColumnCount(4); // 4 个扇区 m_Chart.SetColumnLabel(0, _T("已上线")); m_Chart.SetColumnLabel(1, _T("开发中")); m_Chart.SetColumnLabel(2, _T("测试中")); m_Chart.SetColumnLabel(3, _T("维护中")); double pie[4] = { 5, 12, 7, 3 }; for (int c = 0; c < 4; c++) m_Chart.SetData(0, c, COleVariant(pie[c]));饼图每个扇区的角度由数值比例自动算,总和不一定要是 100,控件会做归一化。这里的坑在于很多人把饼图也想成行和列都要用,结果 RowCount 设成 4,画出来的图完全不对。
饼图的标签是另一个容易纠结的点。扇区名长、数值多的时候,标签会叠成一团。常见做法是关掉扇区上的数值标签,只保留图例:
// 关闭数据标签,只靠图例区分扇区(方法名以头文件为准) m_Chart.GetPlot()->GetDataLabels()->SetShowValue(FALSE);扇区超过 10 个时,别指望 MSChart 能把标签排得开。我一般会在数据处理阶段把占比小于某个阈值的扇区合并成“其他”再喂给控件,这是饼图项目里最实用的经验。
4.3 三类图共用一个绘制函数:按类型分派而不是复制粘贴
实际项目里,柱状图、折线图、饼图经常由一个下拉框切换。与其各写一个函数,不如做一个分派函数:
void CReportDlg::DrawChart(int nType) { switch (nType) { case 0: // 柱状图 m_Chart.SetChartType(1); // 二维柱状图 m_Chart.SetRowCount(6); m_Chart.SetColumnCount(3); FillBarGrid(); // 填 6 行 3 列数据 break; case 1: // 折线图 m_Chart.SetChartType(3); // 二维折线图 // 行列和数据复用柱状图的逻辑 FillBarGrid(); break; case 2: // 饼图 m_Chart.SetChartType(14); // 二维饼图 m_Chart.SetRowCount(1); m_Chart.SetColumnCount(4); FillPieGrid(); // 填 1 行 4 列数据 break; } SetupStyle(); // 坐标轴标题、图例、网格线 m_Chart.Invalidate(); // 触发重绘 }这个函数的设计重点是:FillBarGrid 只管填数据,不管行列结构;行列结构在 switch 里每个 case 自行设置。这样切换图表类型时,数据不会因为重新设置行列而被清空,也不会出现“切到折线图但柱子还在”的残留问题。Invalidate 用来触发控件重绘,MSChart 本身是窗口控件,不需要额外调用刷新方法。
分派函数写好后,下拉框的 BN_SELCHANGE 事件里只需要一行代码调用 DrawChart(当前选中项)。后续加面积图、阶梯图也是同一个套路,改枚举和对应的 Fill 函数,不影响外部调用。
5. MSChart 避坑记录:注册、数据、导出与部署的 5 个典型问题
5.1 现象一:控件在编辑器里是灰框,运行后一片灰
现象:对话框编辑器里能看到一个灰色矩形,程序运行时也是灰的,没有图表区域。原因:控件没有正确注册,或者注册位数和当前工程平台不一致。解决:先用 regsvr32 注册,注意 64 位系统要选对 regsvr32 的路径;注册成功后把对话框上的旧控件删掉重新插入一次,因为资源编辑器可能缓存了错误的控件状态。项目平台和控件位数不一致这种玄学问题,通常重注册一次就能解决。
5.2 现象二:数据设置完毕,图表一片空白或只有一个数据点
现象:SetData 的数据明明填了,但柱状图只有一根柱子或者整张图是空的。原因:SetRowCount 和 SetColumnCount 在 SetData 之后才调用,把数据网格重建了;或者是行标签没设置,导致数据行被折叠。解决:强制顺序,先 SetChartType,再 SetRowCount、SetColumnCount,然后 SetRowLabel、SetColumnLabel,最后 SetData。调试时在填充循环里用 TRACE 输出行号、列号和值,看看是不是行列写反了。
5.3 现象三:饼图标签叠成一团,或者数字不显示
现象:扇区一多,文字叠成黑团,根本看不出哪块是哪个。原因:扇区名太长,或者数据标签同时显示名称和数值,MSChart 又不做自动避让。解决:把列标签缩短,比如“已上线”而不是“当前已经上线的项目数量”;关闭数据标签上的数值,只靠图例区分;扇区数超过 10 个时,在数据侧合并小扇区为“其他”。标签问题是饼图项目里被吐槽最多的点,与其调样式不如从数据源头控制扇区数量。
5.4 现象四:EditCopy 导出图片是黑图或空白
现象:调用 EditCopy 之后,剪贴板里拿到的位图是黑的,或者只有控件背景。原因:控件还没来得及重绘,复制时抓到了无效的绘图表面。解决:在调用 EditCopy 前先执行 Invalidate 加 UpdateWindow,强制控件完成一次绘制;如果还黑,把导出操作延后到消息队列空闲时执行,别在按钮点击事件里紧跟着复制。这个现象在双屏和多分辨率环境下更容易出现,本质是绘制时机问题,和数据无关。
5.5 现象五:程序在自己机器正常,换一台机器图表不显示
现象:绿色拷贝的程序在新机器上跑起来,窗口能弹出来,但图表位置空白,甚至整个对话框创建失败。原因:目标机器没有注册 mschart.ocx,或者缺少对应的 VC 运行库。解决:在安装程序里加入控件注册步骤,而不是依赖用户手动 regsvr32;如果坚持绿色版分发,程序启动时检测控件是否可用,不可用则提示并退出。控件注册失败时,MSChart 的 CreateControl 或资源加载会直接返回失败,日志里能看到 ClassID 相关的错误。
6. 把三类图封装成报表小组件:保存图片函数与一个验证习惯
6.1 保存位图:用 EditCopy 走剪贴板导出 BMP
报表程序通常要把图贴到 Word 或存档,MSChart 没有直接的 SaveToFile 方法,最可靠的做法是先把图表复制到剪贴板,再取出来保存成文件:
#include <atlimage.h> bool SaveChartToBmp(CMSChart& chart, LPCTSTR lpszFile) { chart.EditCopy(); // 控件内容复制到剪贴板 if (!OpenClipboard(chart.GetSafeHwnd())) return false; HANDLE hBmp = GetClipboardData(CF_BITMAP); // 取 GDI 位图 if (hBmp == NULL) { CloseClipboard(); return false; } CImage img; img.Attach((HBITMAP)hBmp); HRESULT hr = img.Save(lpszFile); // 默认按 BMP 保存 img.Detach(); // 位图由剪贴板释放 CloseClipboard(); return SUCCEEDED(hr); }这段代码有两点要注意。第一,剪贴板里可能同时存在 CF_BITMAP 和 CF_DIB,这里优先取 CF_BITMAP 是为了直接挂到 CImage 上;如果拿不到,就改成手工解析 CF_DIB 里的 BITMAPINFOHEADER 写 BMP 文件,代码会长一些但逻辑不复杂。第二,调用前先让控件完成重绘,否则存出来的就是黑图。这个函数我已经在几个内部工具里用过,比截图干净,也比打印到 DC 简单。
6.2 一个验证习惯:先跑最小对话框,再谈封装
每换一个新的开发环境,我会先建一个最小对话框,放一个下拉框和一个 MSChart 控件,把柱状图、折线图、饼图三套数据通过下拉框切换跑一遍,确认控件接口在当前环境里能编译、能出图、能导出。十分钟的事,但能过滤掉大部分接口兼容问题。确认之后再把这个绘制函数挪到正式工程里,正式工程里多半还有中文路径、多线程、报表打印这些干扰因素,如果一开始就没跑通,后面排查方向会乱。
我自己的习惯是,每次接入 MSChart 都用这个最小工程存一份底稿,换个环境先拿它试水,而不是直接改正式项目。这个习惯帮我省了不少查头文件的功夫,也希望帮到你。
本文还有配套的精品资源,点击获取