EternalWindows
オートメーション / Excelの操作

今回は、IDispatchを使用してExcelを操作します。 どのようなオブジェクトが必要になるかを、Excelのウインドウを例に説明します。

Microsoft Excelというタイトルを持つウインドウは、Applicationオブジェクトに関連しています。 このオブジェクトのWorkbooksプロパティを呼び出すとWorkbooksオブジェクトを取得でき、 AddやOpenメソッドを呼び出すことで1つのワークブックを作成することができます。 ワークブックは、複数のワークシートで構成されるウインドウのことであり、 上図では2つ作成されています。 ワークブックはWorkbookオブジェクトで表すことができ、 ActiveSheetプロパティを呼び出せば現在アクティブであるWorksheetオブジェクトを取得できます。 上図でいえば、これはSheet1に相当します。

アプリケーションが書き込み対象とするのは、アクティブであるWorksheetオブジェクトになりますが、 このオブジェクト自身は書き込みを行うためのプロパティを備えていません。 このため、Rangeプロパティを呼び出してRangeオブジェクトを取得し、 このオブジェクトのValueプロパティを通じて書き込みを行うことになります。 各オブジェクトの詳細については、やはりMSDNを参照することになります。

今回のプログラムは、Excelを表示してデータを書き込みます。

#include <windows.h>

HRESULT SetRangeValue(IDispatch *pRange);
HRESULT Invoke(IDispatch *pDispatch, LPOLESTR lpszName, WORD wFlags, VARIANT *pVarArray, int nArgs, VARIANT *pVarResult);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	IDispatch *pApplication;
	IDispatch *pWorkbooks;
	IDispatch *pWorkbook;
	IDispatch *pWorksheet;
	IDispatch *pRange;
	CLSID     clsid;
	HRESULT   hr;
	VARIANT   var;
	VARIANT   varResult;

	CoInitialize(NULL);
	
	hr = CLSIDFromProgID(L"Excel.Application", &clsid);
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}
	
	hr = CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&pApplication));
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}
	
	var.vt = VT_I4;
	var.lVal = 1;
	Invoke(pApplication, L"Visible", DISPATCH_PROPERTYPUT, &var, 1, NULL);

	VariantInit(&varResult);
	Invoke(pApplication, L"Workbooks", DISPATCH_PROPERTYGET, NULL, 0, &varResult);
	pWorkbooks = varResult.pdispVal;
	
	VariantInit(&varResult);
	Invoke(pWorkbooks, L"Add", DISPATCH_PROPERTYGET, NULL, 0, &varResult);
	pWorkbook = varResult.pdispVal;
	
	VariantInit(&varResult);
	Invoke(pWorkbook, L"ActiveSheet", DISPATCH_PROPERTYGET, NULL, 0, &varResult);
	pWorksheet = varResult.pdispVal;
 
	var.vt = VT_BSTR;
	var.bstrVal = SysAllocString(L"A1:E1");
	VariantInit(&varResult);
	Invoke(pWorksheet, L"Range", DISPATCH_PROPERTYGET, &var, 1, &varResult);
	pRange = varResult.pdispVal;
	SysFreeString(var.bstrVal);

	SetRangeValue(pRange);

	pRange->Release();
	pWorksheet->Release();
	pWorkbook->Release();
	pWorkbooks->Release();
	pApplication->Release();
	CoUninitialize();
	
	return 0;
}

HRESULT SetRangeValue(IDispatch *pRange)
{
	int            i;
	SAFEARRAY      *pArray;
	SAFEARRAYBOUND bound[1];
	VARIANT        var, varTmp, *pVar;
	HRESULT        hr;

	bound[0].cElements = 5;
	bound[0].lLbound = 0;
	pArray = SafeArrayCreate(VT_VARIANT, 1, bound);
	
	SafeArrayAccessData(pArray, (void **)&pVar);
	for(i = 0; i < 5; i++) {
		varTmp.vt = VT_I4;
		varTmp.lVal = i;
		pVar[i] = varTmp;
	}
	SafeArrayUnaccessData(pArray);
	
	var.vt = VT_ARRAY | VT_VARIANT;
	var.parray = pArray;
	hr = Invoke(pRange, L"Value", DISPATCH_PROPERTYPUT, &var, 1, NULL);
	
	SafeArrayDestroy(pArray);

	return hr;
}

HRESULT Invoke(IDispatch *pDispatch, LPOLESTR lpszName, WORD wFlags, VARIANT *pVarArray, int nArgs, VARIANT *pVarResult)
{
	DISPPARAMS dispParams;
	DISPID     dispid;
	DISPID     dispidName = DISPID_PROPERTYPUT;
	HRESULT    hr;
	
	hr = pDispatch->GetIDsOfNames(IID_NULL, &lpszName, 1, LOCALE_USER_DEFAULT, &dispid);
	if (FAILED(hr))
		return hr;
	
	dispParams.cArgs = nArgs;
	dispParams.rgvarg = pVarArray;
	if (wFlags & DISPATCH_PROPERTYPUT) {
		dispParams.cNamedArgs = 1;
		dispParams.rgdispidNamedArgs = &dispidName;
	}
	else {
		dispParams.cNamedArgs = 0;
		dispParams.rgdispidNamedArgs = NULL;
	}

	hr = pDispatch->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, wFlags, &dispParams, pVarResult, NULL, NULL);

	return hr;
}

Excelの表にデータを書き込むには、RangeオブジェクトのValueプロパティを呼び出す必要があります。 まずは、このRangeオブジェクトを取得するまでのコードを確認します。

VariantInit(&varResult);
Invoke(pApplication, L"Workbooks", DISPATCH_PROPERTYGET, NULL, 0, &varResult);
pWorkbooks = varResult.pdispVal;
	
VariantInit(&varResult);
Invoke(pWorkbooks, L"Add", DISPATCH_PROPERTYGET, NULL, 0, &varResult);
pWorkbook = varResult.pdispVal;
	
VariantInit(&varResult);
Invoke(pWorkbook, L"ActiveSheet", DISPATCH_PROPERTYGET, NULL, 0, &varResult);
pWorksheet = varResult.pdispVal;
 
var.vt = VT_BSTR;
var.bstrVal = SysAllocString(L"A1:E1");
VariantInit(&varResult);
Invoke(pWorksheet, L"Range", DISPATCH_PROPERTYGET, &var, 1, &varResult);
pRange = varResult.pdispVal;
SysFreeString(var.bstrVal);

ExcelのアプリケーションオブジェクトはWorkbooksというプロパティを実装しており、 これを呼び出すとWorkbooksオブジェクトへのポインタを取得することができます。 この時点ではExcelにまだ表は作成されていませんが、 WorkbooksオブジェクトのAddプロパティを呼び出せば新しい表がされ、 その表を識別するWorkbookオブジェクトへのポインタを取得することができます。 ただし、このオブジェクトからはRangeオブジェクトを取得することができないため、 ActiveSheetプロパティからWorksheetオブジェクトを取得し、 このオブジェクトのRangeプロパティを呼び出すことでRangeオブジェクトのポインタを取得します。 Rangeプロパティを呼び出す場合は、データを設定する範囲を明示しておかなければならないため、 AlからE1までの範囲を意味する"A1:E1"という文字列を指定しています。

SetRangeValueという自作関数は、RangeオブジェクトのValueメソッドを呼び出します。 このメソッドはデータを配列として要求するため、ある程度の準備が必要になります。

HRESULT SetRangeValue(IDispatch *pRange)
{
	int            i;
	SAFEARRAY      *pArray;
	SAFEARRAYBOUND bound[1];
	VARIANT        var, varTmp, *pVar;
	HRESULT        hr;

	bound[0].cElements = 5;
	bound[0].lLbound = 0;
	pArray = SafeArrayCreate(VT_VARIANT, 1, bound);
	
	SafeArrayAccessData(pArray, (void **)&pVar);
	for(i = 0; i < 5; i++) {
		varTmp.vt = VT_I4;
		varTmp.lVal = i;
		pVar[i] = varTmp;
	}
	SafeArrayUnaccessData(pArray);
	
	var.vt = VT_ARRAY | VT_VARIANT;
	var.parray = pArray;
	hr = Invoke(pRange, L"Value", DISPATCH_PROPERTYPUT, &var, 1, NULL);
	
	SafeArrayDestroy(pArray);

	return hr;
}

IDispatchで扱われる配列はSAFEARRAY型でなければなりません。 SAFEARRAYBOUND構造体はSAFEARRAY型の情報を定義することができ、 cElementsは配列の要素数、lLboundは最初のインデックスの値を指定します。 SafeArrayCreateは、SAFEARRAYBOUND構造体からSAFEARRAY型の配列を作成する関数であり、 第1引数はデータの型、第2引数は配列の次元数になります。 よって、上記の場合だと、要素数が5であるVARINAT構造体の一元配列が作成されることになります。 配列に実際にデータを格納するには、SafeArrayAccessDataで配列の先頭アドレスを取得する必要があります。 このとき、他のスレッドが配列にアクセスしないようロック処理も行われます。 データの格納については、単に添え字を使用してデータ(verTmp)を代入するだけで問題ありません。 varTmpが数値型を扱っていることから、SafeArrayCreateの第1引数に数値型を指定すればよかったようにも思えますが、 Excelの表には数値だけでなく文字列なども設定することができます。 よって、どのような型でも扱えるVARIANT型でなければなりません。 データの格納が終了したら、SafeArrayUnaccessDataを呼び出してロックを解除します。 この段階まで来れば後は初期化した配列をparrayに指定し、 vtにparrayの使用を意味するVT_ARRAYとデータがVARIANT構造体であることを示すVT_VARIANTを指定します。 Valueメソッドの呼び出しが終われば配列は不要ですから、SafeArrayDestroyで破棄します。

データを設定する範囲がA1からE1のような場合は一元配列で扱えますが、 A1からE3のような複数行の場合は配列を二次元配列として扱う必要があります。 このような例を次に示します。

HRESULT SetRangeValue(IDispatch *pRange)
{
	int            i, j;
	SAFEARRAY      *pArray;
	SAFEARRAYBOUND bound[2];
	LONG           indices[2];
	VARIANT        var, varTmp;
	HRESULT        hr;

	bound[0].cElements = 3;
	bound[0].lLbound = 0;
	bound[1].cElements = 5;
	bound[1].lLbound = 0;
	pArray = SafeArrayCreate(VT_VARIANT, 2, bound);

	for (i = 0; i < 3; i++) {
		for (j = 0; j < 5; j++) {
			varTmp.vt = VT_I4;
			varTmp.lVal = j;
			
			indices[0] = i;
			indices[1] = j;
			SafeArrayPutElement(pArray, indices, (void *)&varTmp);
		}
	}
	
	var.vt = VT_ARRAY | VT_VARIANT;
	var.parray = pArray;
	hr = Invoke(pRange, L"Value", DISPATCH_PROPERTYPUT, &var, 1, NULL);
	
	SafeArrayDestroy(pArray);

	return hr;
}

二次元配列を作成する場合は、SafeArrayCreateの第2引数に2を指定します。 この場合は、SAFEARRAYBOUND構造体で二次元目の情報も格納しなければならないため、 bound[1]にそれを格納します。 イメージとしては、作成した配列はpArray[3][5]ということになります。 二次元配列にデータを設定する場合は、SafeArrayPutElementが役に立ちます。 この関数の第2引数は、データを設定する要素の行と列を指定した配列であるため、 上記の例であればpArray[i][j]にデータが格納されることになります。

ワークブックからデータを取得する

ワークブックからデータを取得する例を次に示します。

#include <windows.h>

HRESULT GetRangeValue(IDispatch *pRange);
HRESULT Invoke(IDispatch *pDispatch, LPOLESTR lpszName, WORD wFlags, VARIANT *pVarArray, int nArgs, VARIANT *pVarResult);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	IDispatch *pApplication;
	IDispatch *pWorkbooks;
	IDispatch *pWorkbook;
	IDispatch *pWorksheet;
	IDispatch *pRange;
	CLSID     clsid;
	HRESULT   hr;
	VARIANT   var;
	VARIANT   varResult;

	CoInitialize(NULL);
	
	hr = CLSIDFromProgID(L"Excel.Application", &clsid);
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}
	
	hr = CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&pApplication));
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}

	VariantInit(&varResult);
	Invoke(pApplication, L"Workbooks", DISPATCH_PROPERTYGET, NULL, 0, &varResult);
	pWorkbooks = varResult.pdispVal;
	
	var.vt = VT_BSTR;
	var.bstrVal = SysAllocString(L"C:\\sample.xls");
	VariantInit(&varResult);
	hr = Invoke(pWorkbooks, L"Open", DISPATCH_METHOD, &var, 1, &varResult);
	pWorkbook = varResult.pdispVal;
	SysFreeString(var.bstrVal);
	if (FAILED(hr)) {
		MessageBox(NULL, TEXT("ワークブックのオープンに失敗しました。"), NULL, MB_ICONWARNING);
		pWorkbooks->Release();
		pApplication->Release();
		CoUninitialize();
		return 0;
	}
	
	VariantInit(&varResult);
	Invoke(pWorkbook, L"ActiveSheet", DISPATCH_PROPERTYGET, NULL, 0, &varResult);
	pWorksheet = varResult.pdispVal;
 
	var.vt = VT_BSTR;
	var.bstrVal = SysAllocString(L"A1:B2");
	VariantInit(&varResult);
	Invoke(pWorksheet, L"Range", DISPATCH_PROPERTYGET, &var, 1, &varResult);
	pRange = varResult.pdispVal;
	SysFreeString(var.bstrVal);

	GetRangeValue(pRange);
	
	var.vt = VT_BOOL;
	var.boolVal = FALSE;
	Invoke(pWorkbook, L"Close", DISPATCH_METHOD, &var, 1, NULL);
	Invoke(pApplication, L"Quit", DISPATCH_METHOD, NULL, 0, NULL);

	pRange->Release();
	pWorksheet->Release();
	pWorkbook->Release();
	pWorkbooks->Release();
	pApplication->Release();
	CoUninitialize();
	
	return 0;
}

HRESULT GetRangeValue(IDispatch *pRange)
{
	LONG      i, j;
	LONG      lIndexMin1, lIndexMax1;
	LONG      lIndexMin2, lIndexMax2;
	LONG      indices[2];
	SAFEARRAY *pArray;
	VARIANT   var;
	VARIANT   varResult;
	HRESULT   hr;
	TCHAR     szBuf[256];

	VariantInit(&varResult);
	hr = Invoke(pRange, L"Value", DISPATCH_PROPERTYGET, NULL, 0, &varResult);
	if (FAILED(hr))
		return hr;
	pArray = varResult.parray;
	
	SafeArrayGetLBound(pArray, 1, &lIndexMin1);
	SafeArrayGetUBound(pArray, 1, &lIndexMax1);
	SafeArrayGetLBound(pArray, 2, &lIndexMin2);
	SafeArrayGetUBound(pArray, 2, &lIndexMax2);

	for (i = lIndexMin1; i <= lIndexMax1; i++) {
		for (j = lIndexMin2; j <= lIndexMax2; j++) {
			indices[0] = i;
			indices[1] = j;
			VariantInit(&var);
			SafeArrayGetElement(pArray, indices, &var);
			if (var.vt == VT_BSTR) {
				MessageBoxW(NULL, (LPWSTR)var.bstrVal, L"OK", MB_OK);
				VariantClear(&var);
			}
			else if (var.vt == VT_R8) {
				VARIANT varNew;
				VariantInit(&varNew);
				VariantChangeType(&varNew, &var, 0, VT_BSTR);
				MessageBoxW(NULL, (LPWSTR)varNew.bstrVal, L"OK", MB_OK);
				VariantClear(&varNew);
			}
			else if (var.vt == VT_DATE) {
				SYSTEMTIME systemTime;
				VariantTimeToSystemTime(var.date, &systemTime);
				wsprintf(szBuf, TEXT("%d/%d/%d"), systemTime.wYear, systemTime.wMonth, systemTime.wDay);
				MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);
			}
			else if (var.vt == VT_EMPTY)
				MessageBox(NULL, TEXT(""), TEXT("OK"), MB_OK);
			else {
				wsprintf(szBuf, TEXT("予期しないデータ型 %d"), var.vt);
				MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);
			}
		}
	}

	VariantClear(&varResult);

	return hr;
}

HRESULT Invoke(IDispatch *pDispatch, LPOLESTR lpszName, WORD wFlags, VARIANT *pVarArray, int nArgs, VARIANT *pVarResult)
{
	DISPPARAMS dispParams;
	DISPID     dispid;
	DISPID     dispidName = DISPID_PROPERTYPUT;
	HRESULT    hr;
	
	hr = pDispatch->GetIDsOfNames(IID_NULL, &lpszName, 1, LOCALE_USER_DEFAULT, &dispid);
	if (FAILED(hr))
		return hr;
	
	dispParams.cArgs = nArgs;
	dispParams.rgvarg = pVarArray;
	if (wFlags & DISPATCH_PROPERTYPUT) {
		dispParams.cNamedArgs = 1;
		dispParams.rgdispidNamedArgs = &dispidName;
	}
	else {
		dispParams.cNamedArgs = 0;
		dispParams.rgdispidNamedArgs = NULL;
	}

	hr = pDispatch->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, wFlags, &dispParams, pVarResult, NULL, NULL);

	return hr;
}

今回はデータを取得することが目的であるため、Excelのウインドウを表示する必要はありません。 よって、Visibleプロパティは呼び出していません。 データを取得するにはワークブックをオープンしなければならないため、 WorkbooksオブジェクトのOpenメソッドを呼び出してWorkbookオブジェクトを取得します。 ここから先は、データを設定する際と同じような手順でRangeオブジェクトを取得し、 最終的にValueプロパティをDISPATCH_PROPERTYGETで呼び出します。

Valueプロパティの呼び出しに成功したら、SAFEARRAY型の配列を取得することができます。 Rangeオブジェクトを取得する際に指定した範囲がA1:B2だったことから、 この範囲のデータを格納した二次元配列ということになります。 配列からデータを取得するにはSafeArrayGetElementを呼び出すことができますが、 その前に配列のインデックスとして指定できる最小値と最大値を把握しておく必要があります。 最小値はSafeArrayGetLBoundで取得することができ、最大値はSafeArrayGetUBoundで取得することができます。 配列は二次元配列であるため、第2引数の次元数を変えてそれぞれ2回呼び出します。

SafeArrayGetElementの第2引数には、データを取得するインデックスを格納した配列を指定します。 今回はindices[0]にiを、indices[1]にjを指定しているため、イメージとしてはpArray[i][j]からデータを取得することになります。 取得したデータはVARIANT構造体であり、vtメンバを確認することで実際のデータの型を特定することができます。 VT_BSTRの場合はbstrValメンバにBSTR型の文字列が格納されており、 VT_R8の場合はdblValに小数が格納されています。 ただし、Excelでは整数も少数として扱われるようになっており、 VT_R8だけではその値の本当の型が特定できないように思えます。 これでは都合が悪いため、VariantChangeTypeを呼び出すことで強引にVT_BSTR型に変換し、 データを文字列として扱うようにしています。 整数に変換したい場合は、第4引数にVT_I4を指定してvarNew.lValを参照することになるでしょう。 vtメンバがVT_DATEの場合は、dateメンバに日付が格納されていることを意味し、 VariantTimeToSystemTimeを呼び出すことでSYSTEMTIME構造体に変換できます。 VT_EMPTYの場合は、セルにデータが書き込まれていないことを意味します。



戻る