如何在C++项目中优雅地绘制图表
- matplotlib
- prepare
- matplotlibcpp.h
- python3
- vs configure
- test
- Gnuplot
- prepare
- gnuplot
- gnuplot-iostream.h
- boost
- vs configure
- test
- MathGL
在C++项目中,在进行一些数据分析时往往不够直观,若能借助图表进行分析可以达到事半功倍的效果,通常可以将数据写出到文本,再在excel或python上进行图表绘制和分析,那为什么不直接在像python一样,借助三方库进行图表绘制呢?以下将介绍简单易行的方法,尝试在C++中优雅地绘制图表(测试环境:Windows+VS2019)。
matplotlib
实际上是调用Python中的matplotlib提供的C++ API,无需编译,直接包含头文件然后在C++中配置相应python环境就可以,对于熟悉python的伙伴来说几乎没有学习成本(其本身语法也很简单)。
prepare
- 可访问github的网络
- git(非必须)
- python3(< 3.11)
- vs环境
matplotlibcpp.h
有git的直接clone到本地:
git clone https://github.com/lava/matplotlib-cpp.git
没有git可以下载代码或者直接在线复制 matplotlibcpp.h
。
对 matplotlibcpp.h
作如下修改:
新版的
matplotlibcpp.h
中支持了C++17语法,将其改回C++11,且有重定义,需要注释。
将353行至356行代码用以下代码覆盖:
static_assert(sizeof(long long) == 8, "long type must occupy 8 bytes");
//template <> struct select_npy_type<long long> { const static NPY_TYPES type = NPY_INT64; };
static_assert(sizeof(unsigned long long) == 8, "long type must occupy 8 bytes");
//template <> struct select_npy_type<unsigned long long> { const static NPY_TYPES type = NPY_UINT64; };
python3
matplotlibcpp.h
语法与python3.11以上版本不兼容,报错如下:
1>matpoltlibTest.cpp
1>D:\Program Files\MatplotlibCpp\matplotlibcpp.h(174): error C4996: 'Py_SetProgramName': deprecated in 3.11
1>d:\programdata\anaconda3\include\pylifecycle.h(37): note: 参见“Py_SetProgramName”的声明
1>D:\Program Files\MatplotlibCpp\matplotlibcpp.h(182): error C4996: 'PySys_SetArgv': deprecated in 3.11
1>d:\programdata\anaconda3\include\sysmodule.h(13): note: 参见“PySys_SetArgv”的声明
使用3.11以下的python版本即可,这里我创建一个新的虚拟环境:
(base)>conda create -n Env310 python=3.10.16
(base)>conda activate Env310
(Env310)>conda install matplotlib numpy
将 复制到 D:\Program Files\MatplotlibCpp 下(非必须)。
vs configure
创建一个vs项目,切换为Release x64平台,在属性管理器的【Release | 64】下中新增一个属性表:
双击该属性表,开始配置:
【通用属性】-【VC++目录】-【包含目录】添加:
D:\Program Files\MatplotlibCpp
D:\ProgramData\anaconda3\envs\Env310\include
D:\ProgramData\anaconda3\envs\Env310\Lib\site-packages\numpy\_core\include
注意:不同版本的numpy包含目录所在路径可能不同,可通过以下代码确定:
import numpy
print(numpy.get_include())
【通用属性】-【VC++目录】-【库目录】添加:
D:\ProgramData\anaconda3\envs\Env310\libs
【通用属性】-【链接器】-【常规】-【附加库目录】添加:
D:\ProgramData\anaconda3\envs\Env310
【通用属性】-【链接器】-【输入】-【附加依赖项】添加:
python3.lib
保存属性表,在解决方案管理器中右击,更改属性:
【配置属性】-【调试】-【工作目录】更改为:
D:\ProgramData\anaconda3\envs\Env310
test
添加C++测试代码,如:
#include <cmath>
#include "matplotlibcpp.h"namespace plt = matplotlibcpp;#define M_PI 3.141592653589793238432643int main()
{// Prepare data.int n = 5000; // number of data pointsstd::vector<double> x(n), y(n);for (int i = 0; i < n; ++i){double t = 2 * M_PI * i / n;x.at(i) = 16 * sin(t) * sin(t) * sin(t);y.at(i) = 13 * cos(t) - 5 * cos(2 * t) - 2 * cos(3 * t) - cos(4 * t);}// plot() takes an arbitrary number of (x,y,format)-triples.// x must be iterable (that is, anything providing begin(x) and end(x)),// y must either be callable (providing operator() const) or iterable.plt::plot(x, y, "r-", x, [](double d){return 12.5 + abs(sin(d));}, "k-");// show plotsplt::show();
}
运行,可得图片如下,配置完成。
Gnuplot
类似matplotlib,先安装gnuplot本体,然后通过C++API调用库,不同的是其可通过命令在终端中使用,需要一定学习成本。
prepare
- 可访问github的网络
- git(非必须)
- boost
- vs环境
gnuplot
在官网 可找到最新稳定版下载页面,下载编译好的二进制文件。
解压后长这个样子:
在bin
文件夹下双击打开gnupolt.exe
,输入测试代码如下:
plot [-6:6] [-3:3] sin(x),cos(x)
可得:
将整个gunplot
文件夹剪切至D:\Program Files\
下,并重命名为Gnuplot
(个人喜好),然后将D:\Program Files\Gnuplot\bin
添加至系统环境变量Path
中,通过重启或者其他方式确保环境变量生效(打开任意终端,输入gunplot未报错即可)。
那么同样可以在c++代码中调用该exe:
#include <stdio.h>void main()
{FILE* pipe = _popen("gnuplot", "w");if (pipe == NULL){exit(-1);}fprintf(pipe, "set terminal wxt size 600, 400\n");fprintf(pipe, "unset border\n");fprintf(pipe, "set dummy u, v\n");fprintf(pipe, "set angles degrees\n");fprintf(pipe, "set parametric\n");fprintf(pipe, "set view 60, 136, 1.22, 1.26\n");fprintf(pipe, "set samples 64, 64\n");fprintf(pipe, "set isosamples 13, 13\n");fprintf(pipe, "set mapping spherical\n");fprintf(pipe, "set style data lines\n");fprintf(pipe, "unset xtics\n");fprintf(pipe, "unset ytics\n");fprintf(pipe, "unset ztics\n");fprintf(pipe, "set title 'Labels colored by GeV plotted in spherical coordinate system'\n");fprintf(pipe, "set urange [ -90.0000 : 90.0000 ] noreverse nowriteback\n");fprintf(pipe, "set vrange [ 0.00000 : 360.000 ] noreverse nowriteback\n");fprintf(pipe, "set xrange [ * : * ] noreverse writeback\n");fprintf(pipe, "set x2range [ * : * ] noreverse writeback\n");fprintf(pipe, "set yrange [ * : * ] noreverse writeback\n");fprintf(pipe, "set y2range [ * : * ] noreverse writeback\n");fprintf(pipe, "set zrange [ * : * ] noreverse writeback\n");fprintf(pipe, "set cblabel 'GeV'\n");fprintf(pipe, "set cbrange [ 0.00000 : 8.00000 ] noreverse nowriteback\n");fprintf(pipe, "set rrange [ * : * ] noreverse writeback\n");fprintf(pipe, "set colorbox user\n");fprintf(pipe, "set colorbox vertical origin screen 0.9, 0.2 size screen 0.02, 0.75 front noinvert bdefault\n");fprintf(pipe, "NO_ANIMATION = 1\n");fprintf(pipe, "splot cos(u)*cos(v),cos(u)*sin(v),sin(u) notitle with lines lt 5\n");fprintf(pipe, "pause mouse\n");_pclose(pipe);
}
结果如下:
但是,以这种方式,如果要批量绘制自定义的数据,仍需要先存数据为文本,然后再读取,这不是我想要的直接绘制的效果。但是不慌,再做一些简单配置就可以实现了。
gnuplot-iostream.h
有git的直接clone到本地
git clone https://github.com/dstahlke/gnuplot-iostream.git
没有git可以下载代码或者直接在线复制 gnuplot-iostream.h
对 gnuplot-iostream.h
作如下修改
新版的
gnuplot-iostream.h
中支持了C++17语法,将其改回C++11,跟上面的matplotlibcpp.h
是同样类型的改动
168-169行:
static_assert( is_like_stl_container<std::vector<int>>);
static_assert(!is_like_stl_container<int>);
改为:
static_assert(is_like_stl_container<std::vector<int>>, "Should be STL-like container");
static_assert(!is_like_stl_container<int>, "int is not STL-like container");
186行:
static_assert(is_boost_tuple_nulltype<boost::tuples::null_type>);
改为:
static_assert(is_boost_tuple_nulltype<boost::tuples::null_type>, "Should be boost null type");
197-200行:
static_assert( is_boost_tuple<boost::tuple<int>>);
static_assert( is_boost_tuple<boost::tuple<int, int>>);
static_assert(!is_boost_tuple<std::tuple<int>>);
static_assert(!is_boost_tuple<std::tuple<int, int>>);
改为:
static_assert(is_boost_tuple<boost::tuple<int>>, "Should be boost::tuple");
static_assert(is_boost_tuple<boost::tuple<int, int>>, "Should be boost::tuple");
static_assert(!is_boost_tuple<std::tuple<int>>, "std::tuple is not boost::tuple");
static_assert(!is_boost_tuple<std::tuple<int, int>>, "std::tuple is not boost::tuple");
2436-2437行
static_assert( is_eigen_matrix<Eigen::MatrixXf>);
static_assert(!is_eigen_matrix<int>);
改为
static_assert(is_eigen_matrix<Eigen::MatrixXf>, "Should be Eigen::Matrix");
static_assert(!is_eigen_matrix<int>, "int is not an Eigen::Matrix");
2442行
static_assert(dont_treat_as_stl_container<Eigen::MatrixXf>);
改为
static_assert(dont_treat_as_stl_container<Eigen::MatrixXf>, "Eigen::Matrix should not be treated as an STL container");
然后在上述gnuplot的安装目录D:\Program Files\Gnuplot
下新建include
文件夹,将改动后的gnuplot-iostream.h
拷贝过去。
boost
由于gnuplot的C++调用需要boost支持,这里也介绍一下boost的配置,因为有预编译好的库,所以也比较easy。
在boost官网的下载页面找到预编译好的Windows库:
根据所需环境选择对应版本。
这里我根据自己的环境选择较早的1.80.0 VS140版(注意和自己的环境对应)。
双击运行:
更改安装目录:
vs configure
创建一个vs项目,切换为Release x64平台,在属性中将【平台工具集】改为Visual Studio 2015 (v140),在属性管理器的【Release | 64】下中新增一个属性表【Gnupolt_Release_x64.props】
双击该属性表,开始配置
【通用属性】-【VC++目录】-【包含目录】添加:
D:\Program Files\Gnuplot\include
D:\Program Files\boost_1_80_0\boost
【通用属性】-【VC++目录】-【库目录】添加:
D:\Program Files\boost_1_80_0\lib64-msvc-14.0
应用,保存属性表。
test
添加C++测试代码,如:
#include <cmath>
#include <vector>
#include <gnuplot-iostream.h>#define M_PI 3.141592653589793238432643int main()
{// 初始化 Gnuplot 对象Gnuplot gp;// 设置标题和样式gp << "set title 'Sharper Heart Shape'\n";gp << "set size ratio -1\n"; // 确保 x 和 y 轴比例一致gp << "set nokey\n"; // 不显示图例gp << "set samples 1000\n"; // 增加采样点使曲线更平滑gp << "set xrange [-20:20]\n"; // 设置 x 轴范围gp << "set yrange [-20:20]\n"; // 设置 y 轴范围// 定义参数化方程的数据点std::vector<std::pair<double, double>> points;const int num_points = 1000; // 数据点数量for (int i = 0; i <= num_points; ++i){double t = 2 * M_PI * i / num_points; // 参数 t 从 0 到 2πdouble x = 16 * pow(sin(t), 3); // 使用新的 x 方程double y = (13 * cos(t) - 5 * cos(2 * t) - 2 * cos(3 * t) - cos(4 * t)); // 使用新的 y 方程points.emplace_back(x, y);}// 绘制爱心形状gp << "plot '-' with lines lc rgb 'red' lw 2 title 'Heart'\n";for (const auto& point : points){gp << point.first << " " << point.second << "\n";}gp << "e\n"; // 结束数据流return 0;
}
运行,可得图片如下,配置完成。
MathGL
需要特定的环境和复杂的配置,其发布的库也是Linux平台下的,在Windows需要和其他依赖库一起重新编译。
同样在官网找到最新版下载入口,这里有两个版本,我两个版本都下载并配置了,很不幸都在测试阶段报错了,要么缺少头文件要么缺依赖库。虽然可以拿源码自己重新cmake,但其需要依赖其他可视化库(背离了简单易行的初衷),本着世上无难事只要肯放弃的精神,暂停MathGL的测试。
测试代码:
#include <mgl2/mgl.h>int main()
{mglGraph gr;gr.SetRange('x', -1, 1);gr.SetRange('y', -1, 1);gr.Axis();gr.FPlot("sin(1.7*2*pi*x) + sin(1.9*2*pi*x)", "r-2");gr.WriteFrame("test.png");return 0;
}
mathgl-8.0.3-ucrt64
报错:
1>D:\Program Files\MathGL2\include\mgl2\data_cf.h(26,10): fatal error C1083: 无法打开包括文件: “gsl/gsl_vector.h”: No such file or directory
mathgl-8.0.3.LGPL-ucrt64
报错:
1>D:\Program Files\MathGL2\include\mgl2\data.h(27,10): fatal error C1083: 无法打开包括文件: “armadillo”: No such file or directory
打完收工。