ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

VC++ MFC中使用MSChart绘制柱状图、折线图与饼图详解

VC++ MFC中使用MSChart绘制柱状图、折线图与饼图详解 简介面向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最好保证你的应用程序编译目标是x86Win32因为MSChart这个COM控件在64位进程下经常出现莫名奇妙的问题。注意如果你的工程已经从VC6升级到VS2019打开旧对话框资源后MSChart控件仍然会显示为最早的外观样式这是正常的不影响数据输出功能。2.2 在MFC对话框中嵌入MSChart在工程里新建一个基于对话框的MFC应用后我们需要做四件事在资源编辑器中拖入MSChart控件调整大小并设置ID例如IDC_MSCHART为控件添加关联变量。VS2010及更早版本直接右键 - Add VariableVS2015之后版本需要手动在类向导中操作添加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代表柱状图。常用的图表类型包括13D柱状图、22D柱状图、33D折线图、42D折线图、53D面积图、62D面积图、142D饼图、153D饼图。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环境下设置百分比格式的代码稍显繁琐但可以通过属性页提前设置或者使用SetDataPointLabelGetLabelFormat等方式。在实际项目中我更倾向于先设置数据为具体数值然后在列标签里拼好“名称百分比”省去额外格式化操作。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类型数据饼图只显示一块或没显示行数太多或未设置为单行设置RowCount1后重试坐标轴标题不显示未设置轴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以来遇到的八成以上问题。本文还有配套的精品资源点击获取
返回列表