VC++ でエクセルを操作する (グラフ描画)

# 概要
今回は C++ で作成した数値データをエクセルでグラフ化します。

# 大数の法則
題材として[大数の法則](https://ja.wikipedia.org/wiki/%E5%A4%A7%E6%95%B0%E3%81%AE%E6%B3%95%E5%89%87)を取り上げます。

サイコロを何回か振って出目の回数をそれぞれ集計して、理論値 `16.666… %` に近づく様子をグラフ化してみます。[^1]
[^1]:平均の計算方法に間違いがあるようでしたら指摘して下さい。

```C++
#define  WIN32_LEAN_AND_MEAN
#define  _WIN32_WINNT _WIN32_WINNT_WIN7
#include <windows.h>
#include <comdef.h>
#include <iostream>
#include <iomanip>
#include <random>

//Excelを操作するためのタイプライブラリを読みこむ (オフィスのバージョンに依存しない)
//Microsoft Office Object Library
#import "libid:2DF8D04C-5BFA-101B-BDE5-00AA0044DE52" no_auto_exclude auto_rename dual_interfaces
//Microsoft Visual Basic for Applications Extensibillity
#import "libid:0002E157-0000-0000-C000-000000000046" dual_interfaces
//Mcrosoft Excel Object Library
#import "libid:00020813-0000-0000-C000-000000000046" no_auto_exclude auto_search auto_rename dual_interfaces

using namespace Excel; //名前空間の定義

struct StartOle {
	StartOle() { CoInitialize(NULL); }		//COMを初期化
	~StartOle() { CoUninitialize(); }		//COMを閉じる
} _inst_StartOle;

void dump_com_error(_com_error &e)
{
	std::cerr << "Com error!\n";
	std::cerr << "\tCode = " << std::setw(8) << std::hex << e.Error() << '\n';
	std::cerr << "\tCode meaning = " << e.ErrorMessage() << '\n';
	_bstr_t bstrSource(e.Source());
	_bstr_t bstrDescription(e.Description());
	LPCSTR	source = (LPCSTR)bstrSource;
	std::cerr << "\tSource = " << (source ? source : "(NULL)") << "\n";
	LPCSTR descript = (LPCSTR)bstrDescription;
	std::cerr << "\tDescription = " << (descript ? descript : "(NULL)") << "\n";
}

namespace {
	const int SMP  = 30000000;				//サンプル数
	const int SEG  =      100;				//分割数
	const int DICE =        6;				//出目の数
}

//乱数の平均を生成する
void set_data(_WorksheetPtr &sheet)
{
	std::random_device rand_dev;				//非決定的な乱数生成器を生成
	std::mt19937 mt(rand_dev());				//メルセンヌ・ツイスタの32ビット版、引数は初期シード値
	std::uniform_int_distribution<> rand_dice(0, DICE-1);
												//[0, 5] 範囲の一様乱数

	int		sum[DICE] = { 0 };					//1～DICEの出目
	double	ave[DICE][SEG];						//1～DICEの平均値

	for (int x = 0; x < SEG; ++x) {				//分割数(SEG)分繰り返し
		for (int i = 0; i < (SMP / SEG); ++i) {	//サンプル数÷分割数ずつ繰り返し
			int r = rand_dice(mt);				//サイコロの振る
			++sum[r];							//出目を加算する
		}
		for (int i = 0; i < DICE; ++i)
			ave[i][x] = (double)sum[i] / ((SMP / SEG) * (x + 1));
												//平均を求める
	}

	for (int x = 0; x < SEG; ++x) {
		RangePtr cell1 = sheet->Cells->Item[x+1][1];
		cell1->Value2 = (SMP / SEG) * (x + 1);	//サンプル数
		for (int i = 0; i < DICE; ++i) {
			RangePtr cell2 = sheet->Cells->Item[x+1][i + 2];									//セルを指定
			cell2->Value2 = ave[i][x];			//平均値
		}
	}
}

int main(void)
{
	const	int WaitSec = 5;					//休止秒数

	char buff[256];
	_ApplicationPtr xl;

	//---------------------------------------------------------
	//Excelの起動
	HRESULT hr = xl.CreateInstance(L"Excel.Application");

	if (xl) {									//エクセル インスタンスの生成を確認
		xl->Visible[0] = VARIANT_TRUE;			//表示する
		xl->UserControl = VARIANT_TRUE;			//ユーザーの操作を可能にする

		try {									//例外を捕捉
			WorkbooksPtr workbooks = xl->Workbooks;
												//WorkBookを追加
			_WorkbookPtr workbook = workbooks->Add(xlWorksheet);
												//ワークシートを追加
			_WorksheetPtr worksheet = workbook->ActiveSheet;
												//アクティブ・シート

			//データ作成
			set_data(worksheet);

			RangePtr range = worksheet->Range["B1:B100"][vtMissing];
												//グラフ化する範囲を指定

			//グラフ作成
			_ChartPtr chart = workbook->Charts->Add();
												//グラフを追加
			chart->SetSourceData(range, xlColumns);
												//データソースを指定

			chart->HasTitle[0] = VARIANT_TRUE;	//タイトルあり
			bstr_t	title{ "大数の法則 (サイコロ 出目の確率)" };
			chart->ChartTitle->Caption = title;	//タイトル指定
			chart->ChartType = xlXYScatterSmoothNoMarkers;
												//散布グラフ（平滑線マーカーなし）

			chart = chart->Location(xlLocationAsObject, bstr_t("Sheet1"));
												//オブジェクトとして埋め込む
			chart->HasLegend[0] = VARIANT_TRUE;	//凡例あり
			chart->Legend->Position = xlLegendPositionBottom;
												//凡例位置を下に

			char	xValues[256] = "=Sheet1!R1C1:R100C1";
												//x軸
			SeriesPtr series1 = chart->SeriesCollection(1);
												//系列1
			series1->XValues = variant_t(xValues);
												//Xの値
			series1->Name = bstr_t("1の目");	//凡例1
			series1.Release();

			AxisPtr axis1 = chart->Axes(xlCategory, xlPrimary);
												//X軸
			axis1->MaximumScale = SMP;			//最大値
			axis1.Release();

			AxisPtr axis2 = chart->Axes(xlValue, xlPrimary);
												//Y軸
			axis2->HasMajorGridlines = VARIANT_TRUE;
												//目盛線あり
			axis2.Release();

			SeriesCollectionPtr collection = chart->SeriesCollection();
												//データ系列のコレクション

			//系列追加
			for (int i = 1; i < DICE; ++i) {	//出目のループ
				chart->Refresh();				//グラフ更新

				SeriesPtr series = collection->NewSeries();
												//系列を追加
				char	yValues[256];
				_snprintf_s(yValues, _TRUNCATE, "=Sheet1!R1C%d:R100C%d", i+2, i+2);
				series->Values = variant_t(yValues);
												//データを指定
				series->XValues = variant_t(xValues);
												//Xの値

				char	legend[256];
				_snprintf_s(legend, _TRUNCATE, "%dの目", i + 1);
				series->Name = bstr_t(legend);	//系列名指定
				series.Release();
			}
			//グラフの位置とサイズの調整
			ShapePtr shape = worksheet->Shapes->Item(1);
			shape->Left   = 240.;				//左端
			shape->Top    =  10.;				//上端
			shape->Width  = 640.;				//横幅
			shape->Height = 480.;				//高さ

			chart->ChartArea->Copy();			//グラフをコピー


			//COMオブジェクトを解放する
			collection.Release();
			shape.Release();
			chart.Release();
			range.Release();
			worksheet.Release();

			//WaitSec秒休止
			::Sleep(WaitSec * 1000);

			//ウィンドウを最小化
			xl->WindowState[0] = xlMinimized;
			std::cout << "確認のために一時停止:";
			std::cin.getline(buff, sizeof buff);

			workbook->Saved[0] = VARIANT_TRUE;	//警告メッセージが出ないようにする
			workbook->Close();					//ワークシートを閉じる
			workbook.Release();					//COMオブジェクトを解放する
			workbooks.Release();				//COMオブジェクトを解放する
		} catch (_com_error &e) {				//COMの例外処理
			dump_com_error(e);					//エラーログを出力
		}
		//Excelを終了する
		xl->Quit();
		xl.Release();							//COMオブジェクトを解放する
	} else {
		std::cerr << "エクセルを起動できません\n";
	}

	std::cout << "テストプログラムを終了:";
	std::cin.getline(buff, sizeof buff);
	return 0;
}
```

エクセルの起動など以前のプログラムと同様です。

`set_data()`で`SMP`回サイコロを振って、区分ごとの各目の平均を求めます。求めた平均回数をエクセルのセルに出力します。
```C++
	//グラフ作成
	_ChartPtr chart = workbook->Charts->Add();
	chart->SetSourceData(range, xlColumns);
```
グラフを追加し、データソースを指定します。

最初に一つだけ系列 (1の目) を指定して、後で他の系列 (2～6の目) を追加します。

```C++
	chart->HasTitle[0] = VARIANT_TRUE;
	bstr_t	title{ "大数の法則 (サイコロ 出目の確率)" };
	chart->ChartTitle->Caption = title;
	chart->ChartType = xlXYScatterSmoothNoMarkers;
```
タイトルやグラフの種類を指定します。

```C++
	//系列追加
	for (int i = 1; i < DICE; ++i) {
		chart->Refresh();
		SeriesPtr series = collection->NewSeries();
		char	yValues[256];
		_snprintf_s(yValues, _TRUNCATE, "=Sheet1!R1C%d:R100C%d", i+2, i+2);
		series->Values = variant_t(yValues);
		series->XValues = variant_t(xValues);
		char	legend[256];
		_snprintf_s(legend, _TRUNCATE, "%dの目", i + 1);
		series->Name = bstr_t(legend);
		series.Release();
	}
```
`2～6の目`の系列を追加します。

```C++
	chart->ChartArea->Copy();
```
作成したグラフをクリップボードにコピーします。

# 実行結果
![大数の法則](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/207428/4dec7bc4-73b8-9ce3-57d5-09481f1dd544.png)
試行回数が多くなると理論値`0.1666…`に近づくことがわかります。

# 開発環境および実行環境
- Windows10
- Visual Studio 2019 Community
- Excel 2013