VC++ でエクセルのセル範囲に配列データを書込む

# 概要
[前々回](https://qiita.com/BenjaminBenton7/items/52cfcfe493551e3b69f9)、[前回](https://qiita.com/BenjaminBenton7/items/a411930d09488e0b55f3)と`VC++`で`エクセル`を操作するプログラムを投稿しましたが、今回も引き続き`VC++`のプログラムで`エクセル`を操作します。

これまでは単体のデータの読み書きを行ってきました。複数のデータを扱うためには、ループで列と行を指定したセルを対象にすればいいのですが、これを大量に行うと非常に遅くなります。

そこで、セルの範囲を指定してまとめて読み書きすることを考えます。セル範囲に書込むデータは二次元配列になりますが、`SAFEARRAY`という特別な配列を使用することになります。

`SAFEARRAY`の扱いはちょっと面倒なのですが、データの格納と取出しができればとりあえずはいいんじゃないかと思います。

# SAFEARRAY の使い方
`SAFEARRAY`は`COM`でデータ(配列)をやりとりするときに使われる`<OAIdl.h>`に定義されいる構造体で次のようなものです。
## SAFEARRAY 構造体
```c++
typedef struct tagSAFEARRAY
    {
    USHORT cDims;
    USHORT fFeatures;
    ULONG cbElements;
    ULONG cLocks;
    PVOID pvData;
    SAFEARRAYBOUND rgsabound[ 1 ];
    } 	SAFEARRAY;
```
|Member|説明|
|:--|:--|
|cDims|次元|
|fFeatures|フラグ|
|cbElements|要素のサイズ|
|cLocks|ロック回数|
|pvData|データのアドレス|
|rgsabound|各次元の下限と要素数|

大切なのは`cDims`と`rgsabound`です。
次元と要素数が決まれば、配列の構造が分かります。
`fFeatures`と`cLocks`をプログラムで直接扱うことはないと思われます。

## SAFEARRAYを扱う関数 (主なもの)
|関数|説明|
|:--|:--|
|SafeArrayCreate|配列生成|
|SafeArrayGetDim|配列の次元数を取得|
|SafeArrayGetLBound|各次元の下限を取得|
|SafeArrayGetUBound|各次元の上限を取得|
|SafeArrayGetElement|指定された要素を取得
|SafeArrayPutElement|データを指定された位置に格納
|SafeArrayAccessData|ロックしてデータのアドレスを取得|
|SafeArrayUnaccessData|配列をアンロック
|SafeArrayDestroy|配列を破棄|

## 一連の流れ
配列の次元と各下限と要素数を指定して、配列を生成します。
データ アドレスで直接配列にアクセスするか、インデクスを指定してデータを格納あるいは取得します。
使い終わったら配列を破棄します。

## メモリ配置
`SafeArrayAccessData`を用いれば配列のアドレスを取得できるので、ポインタで直接アクセスできます。ただし、一次元配列は問題ないのですが、二次元以上配列では`C/C++`とはメモリ配置が違うので、ちょっと扱いづらいです。

参考 : [SAFEARRAY 構造体](https://w.atwiki.jp/ipubluedictionary/pages/105.html#6)

# サンプルプログラム
```C++
#include <windows.h>
#include <iostream>
#include <iomanip>
#include <string>
#include <comutil.h>

#pragma warning (disable:4192)
//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;										//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";
}

// データ書込み
void set_data(_WorksheetPtr &sheet, long rows, long cols)
{
	//SafeArrayデータ生成
	variant_t arr;
	arr.vt = VT_ARRAY | VT_VARIANT;
	SAFEARRAYBOUND sab[2];
	sab[0].lLbound = 1; sab[0].cElements = rows;
	sab[1].lLbound = 1; sab[1].cElements = cols;
	arr.parray = SafeArrayCreate(VT_VARIANT, 2, sab);

	//セルに書込むデータを用意する
	for (int i = 0; i < rows; ++i) {
		for (int j = 0; j < cols; ++j) {
			variant_t val{ (i + 1) * cols + j + 1, VT_I4 };

			long indices[] = { i + 1, j + 1 }; 						//インデックスを設定
			SafeArrayPutElement(arr.parray, indices, (void *)&val); //SAFEARRAYに値を格納
		}
	}

	//セルの範囲を決定
	variant_t vtRowBegin(1); 										//開始行
	variant_t vtColBegin(1); 										//開始列
	variant_t vtRowEnd(rows);										//終了行
	variant_t vtColEnd(cols);										//終了列

	variant_t st = sheet->Cells->Item[vtRowBegin][vtColBegin]; 		//開始セル
	variant_t ed = sheet->Cells->Item[vtRowEnd][vtColEnd]; 			//終了セル

	//セル範囲に書込む
	sheet->Range[st][ed]->Value2 = arr;
}

int main(void)
{
	_ApplicationPtr xl;								//エクセル インスタンス

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

	if (hr >= 0) {									//エクセル インスタンスの生成を確認
		xl->Visible[0] = VARIANT_TRUE;				//エクセルを表示する
		xl->DisplayAlerts[0] = VARIANT_FALSE;		//警告が出ないように

		try {										//例外を捕捉
			//-------------------------------------------------
			//新規にワークブックを追加
			WorkbooksPtr workbooks = xl->Workbooks;	//ワークブック コレクション
			_WorkbookPtr workbook = workbooks->Add((long)xlWorksheet);

			//アクティブ・シートを取得
			_WorksheetPtr worksheet = xl->ActiveSheet;

			//セル範囲にデータを書込む
			set_data(worksheet, 5, 10);

			worksheet.Release();					//COM オブジェクトを解放

			//確認のために一時停止
			::Sleep(4 * 1000);
			xl->WindowState[0] = xlMinimized;		//ウィンドウを最小化
			std::cout << "エクセルの起動およびデータの書込みを確認:";
			std::string s;
			std::getline(std::cin, s);

			//ワークブックを保存
			bstr_t filename{ "Sample02.xlsx" };
			workbook->SaveAs(						//ワークシートを保存
				filename,							//保存するファイル名
				vtMissing,							//ファイルのフォーマットを指定
				vtMissing,							//読み取りパスワードを設定
				vtMissing,							//書き込みパスワードを設定
				vtMissing,							//ファイルを開く時、読み取り専用で開くことを推奨するメッセージを表示するかどうかの設定
				vtMissing,							//バックアップ・ファイルを作成するかどうかの指定
				Excel::xlNoChange					//アクセスモードの指定
			);

			//ワークブックを閉じる
			workbook->Close();
			workbook.Release();						//COM オブジェクトを解放
			workbooks.Release();					//COM オブジェクトを解放
		}
		catch (_com_error& e) {						//例外処理
			dump_com_error(e);
		}

		//Excelを閉じる
		xl->Quit();
		xl.Release();								//COM オブジェクトを解放
	} else {										//エクセルを起動できない
		std::cerr << "エクセルを起動できません\n";
	}

	std::cout << "テストプログラムを終了:";
	std::string s;
	std::getline(std::cin, s);
}
```

# プログラムの説明
`main()`での処理はこれまでとあまり変わりません。
エクセルを起動して、新規ワークブックを追加、アクティブシートを取得してデータの書込みを行い、最後に保存して終了です。

`set_data()`でデータをセルに書込みます。
## SAFEARRAY生成
```C++
	variant_t arr;
	arr.vt = VT_ARRAY | VT_VARIANT;
	SAFEARRAYBOUND sab[2];
	sab[0].lLbound = 1; sab[0].cElements = rows;
	sab[1].lLbound = 1; sab[1].cElements = cols;
	arr.parray = SafeArrayCreate(VT_VARIANT, 2, sab);
```
まず、書込み用の`VARIANT`型の変数を定義します。ここではラッパークラスである`variant_t`を使います。

`vt`には`VARIANT`型の配列を表す`VT_ARRAY | VT_VARIANT`をセットします。

引数の行数と列数に従って、配列`SAFEARRAY`を生成します。`SafeArrayCreate()`関数はメモリの確保に失敗すると`NULL`を返すので、本来はチェックすべきですが、ここではプログラムを簡単にするために省略しています。

## データ作成
```C++
	for (int i = 0; i < rows; ++i) {
		for (int j = 0; j < cols; ++j) {
			variant_t val{ (i + 1) * cols + j + 1, VT_I4 };

			long indices[] = { i + 1, j + 1 }; 						//インデックスを設定
			SafeArrayPutElement(arr.parray, indices, (void *)&val); //SAFEARRAYに値を格納
		}
	}
```
配列が生成できたらデータをセットします。ここでは`SafeArrayPutElement()`関数を使用しています。

## セルに書込み
```C++
//セルの範囲を決定
	variant_t vtRowBegin(1); 										//開始行
	variant_t vtColBegin(1);										//開始列
	variant_t vtRowEnd(rows);										//終了行
	variant_t vtColEnd(cols);										//終了列

	variant_t st = sheet->Cells->Item[vtRowBegin][vtColBegin]; 		//開始セル
	variant_t ed = sheet->Cells->Item[vtRowEnd][vtColEnd]; 			//終了セル

	//セル範囲に書込む
	sheet->Range[st][ed]->Value2 = arr;
```
セル範囲の指定はちょっと面倒ですが、`Cells->Item[][]`を使用して、開始セルと終了セルを決定し、`Range[][]->Value2`で書込みます

なお、`variant_t`を使用しているのでデストラクタで配列のメモリは解放されます。使い終わった後に`SafeArrayDestroy()`関数で破棄する必要はないです。
(`SafeArrayDestroy()`すると二重解放でエラーになります)

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