C++BuilderからOLEでExcelを制御しファイルを読む

概説

Windowsではデータ管理にExcelが使われる事が多いので、Excel形式で保存されているデータが多々あります。

C++BuilderでExcel形式のファイルを読む時、Excelのファイル構造がわからなくても、OLEを使ってExcelをコントロールしてファイルを読みデータを取り込むことができます。

ここでは、Variantを使ってOLEでExcelを制御するサンプルを挙げます。

OpenOffice CalcもOLEを使ってExcelと同様の処理が可能ですのでそのサンプルも挙げます。

サンプルプログラムの外観

サンプルプログラムは以下のように1つのフォームとその上に複数のボタン、ファイル名を指定するEdit、データを表示するStringGrid、メッセージを表示するRichEditを配置したものです。
ボタン1〜4に次のように処理を割り当てます。
Button1:Excelの例
Button2:Open Office Calcの例
Button3:Excelをクラス化した例
Button4:Open Office Calcをクラス化した例

サンプルプログラムの外観


Button1:Excelの例

ExcelをOLEで制御するサンプルプログラムは次のとおりです。

void __fastcall TForm1::Button1Click(TObject *Sender)
{
// エクセルの内容をStringGrid1に表示する
// StringGrid1をクリアする
StringGrid1->ColCount = 5;
StringGrid1->RowCount = 5;
for (int i = 0; i < 5; i++)
StringGrid1->Rows[i]->CommaText = ",,,,";

DWORD dw0 = ::GetTickCount();
Variant objExcel, workbook, worksheet, row, range;
int errcnt = 0;
try {
objExcel = Variant::CreateObject("Excel.Application"); // 注1
// Excelを非表示
objExcel.OlePropertySet("Visible", false);
// 警告やメッセージを表示しない
objExcel.OlePropertySet("DisplayAlerts", false);
// ファイルを読む
workbook = objExcel.OlePropertyGet("WorkBooks").
OleFunction("Open", (OleVariant)Edit1->Text); // OK
//OleFunction("Open", (Variant)Edit1->Text); // 注2
//アクティブシート
worksheet = workbook.OlePropertyGet("ActiveSheet");
//使用している範囲
range = worksheet.OlePropertyGet("UsedRange");
}
catch (Exception &e) {
RichEdit1->Lines->Add(e.Message);
errcnt++;
}
if (errcnt == 0) {
Variant var_array;
TStringList *list = new TStringList;
try {
int ncols = range.OlePropertyGet("Columns").OlePropertyGet("Count");
int nrows = range.OlePropertyGet("Rows").OlePropertyGet("Count");
for (int irow = 1; irow <= nrows; irow++) {
row = range.OlePropertyGet("Rows", irow);
var_array = row.OlePropertyGet("Value2");
// 行は2次元配列が返る 注3
int n = var_array.ArrayDimCount();
if (n >= 2) {
int l1 = var_array.ArrayLowBound(1);
//int h1 = var_array.ArrayHighBound(1);
int l2 = var_array.ArrayLowBound(2);
int h2 = var_array.ArrayHighBound(2);
if (h2 > ncols)
h2 = ncols;
list->Clear();
// 行の先頭は行番号
list->Insert(0, String(irow));
for (int i = l2; i <= h2; i++)
list->Add(var_array.GetElement(l1, i));
if (StringGrid1->ColCount < list->Count)
StringGrid1->ColCount = list->Count;
StringGrid1->RowCount = StringGrid1->FixedRows + irow;
StringGrid1->Rows[irow]->Assign(list);
}
}
}
catch (Exception &e) {
RichEdit1->Lines->Add(e.Message);
errcnt++;
}
objExcel.OleProcedure("Quit");

// StringGridにカラム見出し A,B,... を付ける
list->Clear();
list->Add("");
for (int i = StringGrid1->FixedCols; i < StringGrid1->ColCount; i++) {
int icol = i - StringGrid1->FixedCols;
String scol = (char)('A' + (icol % 26));
int cx = icol / 26;
while (cx > 0) {
cx--;
scol = String((char)('A' + (cx % 26))) + scol;
cx = cx / 26;
}
list->Add(scol);
}
StringGrid1->Rows[0]->Assign(list);
delete list;
}
objExcel = Unassigned;
workbook = Unassigned;
worksheet = Unassigned;
range = Unassigned;

DWORD dw9 = ::GetTickCount();
RichEdit1->Lines->Add(String("経過時間 = ") + (dw9 - dw0));
RichEdit1->Perform(EM_SCROLL, SB_LINEDOWN, 0);
}

注1

ここで使用したVariant::CreateObject()の他にCreateOleObject()というのもあります。 Variantクラスのstatic関数であるCreateObject()では、CreateOleObject()を呼び出しているのでどちらも同じように使えます。
「C++Builder Tips - C++Builder プログラミング情報 - Delphi との比較 -」内の「5.Variant型とVariantクラス」を参照

注2

OleFunction()にパラメータを文字列で渡す時、VariantにキャストするとエラーになるのでOleVariantにキャストしなければならない。ネットの調べるとWideStringでも良いらしい(確認済み)。
VariantとOleVariantの違いはembarcaderoのサイトによると次のようになっているが意味が良くわからない。

「OleVariant は OLE 互換の Variant です。Variant と OleVariant の主な違いは、Variant には現在のアプリケーションだけが扱い方を知っているデータ型を格納できることです。OleVariant には、OLE オートメーションと互換性があるように定義されたデータ型のみ格納できます。つまり、格納されているデータ型は、相手がデータの扱い方を知っているかどうかを気にせずにプログラム間またはネットワーク経由で渡すことができるということです。」

注3

1セル毎に値を取得するよりもある程度の範囲をまとめて取得するほうがスピードが断然速い。
ここでは行単位で取得している。



Button2:OpenOffice Calcの例

OpenOffice(or LibreOffice) CalcをOLEで制御するサンプルプログラムは次のとおりです。

void __fastcall TForm1::Button2Click(TObject *Sender)
{
// StringGrid1のクリア
StringGrid1->ColCount = 5;
StringGrid1->RowCount = 5;
for (int i = 0; i < 5; i++)
StringGrid1->Rows[i]->CommaText = ",,,,";

DWORD dw0 = ::GetTickCount();

// OpenOffice (or LibreOffice) でファイルを読む
Variant vOpenOffice; //サービスマネージャ
Variant vStarDesktop; //サービス
Variant vDocument; //ドキュメント
TStringList *list = new TStringList;

try {
//サービスマネージャ生成 注1
vOpenOffice = Variant::CreateObject("com.sun.star.ServiceManager");
//サービス生成
vStarDesktop = vOpenOffice.OleFunction(
"createInstance",
(OleVariant)"com.sun.star.frame.Desktop");
// ファイルを読む
// 読み込み時のオプション指定で非表示、ReadOnlyにする
Variant props = VarArrayCreate(OPENARRAY(int, (0, 1)), varVariant); // 注2
Variant prop = vOpenOffice.OleFunction(
"Bridge_GetStruct",
(OleVariant)"com.sun.star.beans.PropertyValue");
// 非表示 注3
prop.OlePropertySet("Name", (OleVariant)"Hidden");
prop.OlePropertySet("Value", true);
props.PutElement(prop, 0);
prop = vOpenOffice.OleFunction(
"Bridge_GetStruct",
(OleVariant)"com.sun.star.beans.PropertyValue");
// ReadOnly
prop.OlePropertySet("Name", (OleVariant)"ReadOnly");
prop.OlePropertySet("Value", true);
props.PutElement(prop, 1);
vDocument = vStarDesktop.OleFunction(
"loadComponentFromURL",
(OleVariant)ConvertToURL(Edit1->Text), // 注4
(OleVariant)"_blank",
(OleVariant)0,
props);
#if 0
// 新規作成の場合
vDocument = vStarDesktop.OleFunction(
"loadComponentFromURL",
(OleVariant)"private:factory/scalc",
(OleVariant)"_blank",
(OleVariant)0,
dummyArray());
#endif
Variant sheet, range, cell, addr, cursor;
Variant varray;

// 先頭の(0番目)シート
sheet = vDocument.OleFunction("getSheets").OleFunction("getByIndex", 0);

// 使用範囲をカーソルで求める 注5
cursor = sheet.OleFunction("createCursor");
// カーソルを使用範囲の先頭に移動
cursor.OleProcedure("gotoStartOfUsedArea", false);
cell = cursor.OleFunction("getCellByPosition", 0, 0);
addr = cell.OlePropertyGet("CellAddress");
int c0 = addr.OlePropertyGet("Column");
int r0 = addr.OlePropertyGet("Row");
// カーソルを使用範囲の最後に移動
cursor.OleProcedure("gotoEndOfUsedArea", false);
cell = cursor.OleFunction("getCellByPosition", 0, 0);
addr = cell.OlePropertyGet("CellAddress");
int c1 = addr.OlePropertyGet("Column");
int r1 = addr.OlePropertyGet("Row");

RichEdit1->Lines->Add(String("c0 = ") + c0 + "; r0 = " + r0);
RichEdit1->Lines->Add(String("c1 = ") + c1 + "; r1 = " + r1);
RichEdit1->Perform(EM_SCROLL, SB_LINEDOWN, 0);

int isgrow = StringGrid1->FixedRows;
for (int irow = r0; irow <= r1; irow++, isgrow++) {
int idx0, idx1;
// 使用範囲の1行
range = sheet.OleFunction("getCellRangeByPosition", c0, irow, c1, irow);
varray = range.OleFunction("getDataArray");
// varrayは、配列の配列になっている
idx0 = varray.ArrayLowBound(1);
varray = varray.GetElement(idx0);
idx0 = varray.ArrayLowBound(1);
idx1 = varray.ArrayHighBound(1);
list->Clear();
// 行の先頭は行番号
list->Insert(0, String(isgrow));
for (int ii = idx0; ii <= idx1; ii++)
list->Add(varray.GetElement(ii));
if (StringGrid1->ColCount < list->Count)
StringGrid1->ColCount = list->Count;
StringGrid1->RowCount = isgrow + 1;
StringGrid1->Rows[isgrow]->Assign(list);
}

// StringGridにカラム見出し A,B,... を付ける
list->Clear();
list->Add("");
for (int i = StringGrid1->FixedCols; i < StringGrid1->ColCount; i++) {
int icol = i - StringGrid1->FixedCols;
String scol = (char)('A' + (icol % 26));
int cx = icol / 26;
while (cx > 0) {
cx--;
scol = String((char)('A' + (cx % 26))) + scol;
cx = cx / 26;
}
list->Add(scol);
}
StringGrid1->Rows[0]->Assign(list);
}
catch (Exception &e) {
RichEdit1->Lines->Add(e.Message);
}

if (vDocument.Type() == varDispatch)
vDocument.OleProcedure("dispose");
if (vStarDesktop.Type() == varDispatch) {
// 他のコンポーネント(文書)が無ければ terminate 注6
Variant venum = vStarDesktop.OleFunction("getComponents")
.OleFunction("createEnumeration");
if (!(bool)venum.OleFunction("hasMoreElements"))
vStarDesktop.OleProcedure("terminate");
}
vDocument = Unassigned;
vStarDesktop = Unassigned;
vOpenOffice = Unassigned;

delete list;

DWORD dw9 = ::GetTickCount();
RichEdit1->Lines->Add(String("経過時間 = ") + (dw9 - dw0));
RichEdit1->Perform(EM_SCROLL, SB_LINEDOWN, 0);
}

注1

基本的な手順は以下のPDF文書の36ページからの「Q3 OpenOfficeの利用」を参考にしました(但し、C++BuilderでなくDelphiです)。
「知って得する!現役ヘルプデスクが答える Delphiテクニカルエッセンス 5.0」

注2

OPENARRAYはマクロで、DelphiのObject Pascalの「オープン配列」をC++Builderで使えるようにするためのものです。
参照URL:「オープン配列」

注3

参照URL:「service MediaDescriptor」

注4

ファイル名はURLの書式で指定します。ローカルファイルはfile:///で始まる形式になります。
関数ConvertToURL()は同一ソースファイル内で定義しています。

注5

OpenOfficeにはExcelのUsedRangeに相当するプロパティはないが、カーソルのgotoStartOfUsedAreaとgotoEndOfUsedAreaで使用範囲を得ることができます。
参照URL:「interface XUsedAreaCursor」

注6

Desktopのterminateを実行するとOpenOfficeの全ての文書が閉じてしまうので、disposeした後、文書があるかを調べます。


dummyArray()とConverToURL()は次の通りです。

OleVariant dummyArray()
{
return VarArrayCreate(OPENARRAY(int, (0, -1)), varVariant);
}
//---------------------------------------------------------------------------
String ConvertToURL(const String &path)
{
String rtn = "file:"; // 注1
String svv = ExpandUNCFileName(path);
if (svv.Length() > 0 && svv[1] != '\\')
rtn += String("///");
for (int i = 1; i <= svv.Length(); i++) {
#if __BORLANDC__ < 0x600
// 注2
if (svv.IsLeadByte(i)) {
rtn += svv.SubString(i, 2);
i++;
continue;
}
#endif
switch (svv[i]) {
case '\\': rtn += String('/'); break;
case '%': rtn += String("%25"); break;
case ' ': rtn += String("%20"); break;
default: rtn += String(svv[i]);
}
}
return rtn;
}

注1

文字列型には String を使っています。String はC++Builderのバージョンにより AnsiString か UnicodeString にtypedefされているので、バージョンに依存しないコードにできます。

注2

注1ではStringを使うことでバージョンに依存しないコードにできる、と述べたが、C++Builderのバージョンで文字コードがシフトJISの場合、漢字コードの2バイト目に出現する0x5Cが「\」として処理されてしまうので、漢字コードの1バイト目を判定し処理を分けています。



Button3:Excelをクラス化した例

ExcelをOLEで制御する処理をクラス化したサンプルプログラムは次のとおりです。
クラス CPfExcel のソースはこちらです。

void __fastcall TForm1::Button3Click(TObject *Sender)
{
// Excel - クラス版
StringGrid1->ColCount = 5;
StringGrid1->RowCount = 5;
for (int i = 0; i < 5; i++)
StringGrid1->Rows[i]->CommaText = ",,,,";

DWORD dw0 = ::GetTickCount();
CPfExcel pfxl;
if (!pfxl.Open(Edit1->Text))
return;
TStringList *list = new TStringList;
int ncol = pfxl.GetColCount();
int nrow = pfxl.GetRowCount();
StringGrid1->ColCount = StringGrid1->FixedCols + ncol;
StringGrid1->RowCount = StringGrid1->FixedRows + nrow;
int irow;
for (irow = 0; irow < nrow; irow++) {
list->Clear();
list->Add(irow+1); // 先頭に行番号
if (!pfxl.GetRow(irow, list))
break;
StringGrid1->Rows[irow+1]->Assign(list);
}
// StringGridにカラム見出し A,B,... を付ける
list->Clear();
list->Add("");
for (int i = StringGrid1->FixedCols; i < StringGrid1->ColCount; i++) {
int icol = i - StringGrid1->FixedCols;
String scol = (char)('A' + (icol % 26));
int cx = icol / 26;
while (cx > 0) {
cx--;
scol = String((char)('A' + (cx % 26))) + scol;
cx = cx / 26;
}
list->Add(scol);
}
StringGrid1->Rows[0]->Assign(list);

pfxl.Close();
delete list;

DWORD dw9 = ::GetTickCount();
RichEdit1->Lines->Add(String("経過時間 = ") + (dw9 - dw0));
RichEdit1->Perform(EM_SCROLL, SB_LINEDOWN, 0);
}


Button4:OpenOffice Calcをクラス化した例

OpenOffice CalcをOLEで制御する処理をクラス化したサンプルプログラムは次のとおりです。
クラス CPfOOCalc のソースはこちらです。

void __fastcall TForm1::Button4Click(TObject *Sender)
{
// Open Office Calc - クラス版
StringGrid1->ColCount = 5;
StringGrid1->RowCount = 5;
for (int i = 0; i < 5; i++)
StringGrid1->Rows[i]->CommaText = ",,,,";

DWORD dw0 = ::GetTickCount();
CPfOOCalc pfxl; // 注1
if (!pfxl.Open(Edit1->Text))
return;
TStringList *list = new TStringList;
int ncol = pfxl.GetColCount();
int nrow = pfxl.GetRowCount();
StringGrid1->ColCount = StringGrid1->FixedCols + ncol;
StringGrid1->RowCount = StringGrid1->FixedRows + nrow;
int irow;
for (irow = 0; irow < nrow; irow++) {
list->Clear();
list->Add(irow+1); // 先頭に行番号
if (!pfxl.GetRow(irow, list))
break;
StringGrid1->Rows[irow+1]->Assign(list);
}
// StringGridにカラム見出し A,B,... を付ける
list->Clear();
list->Add("");
for (int i = StringGrid1->FixedCols; i < StringGrid1->ColCount; i++) {
int icol = i - StringGrid1->FixedCols;
String scol = (char)('A' + (icol % 26));
int cx = icol / 26;
while (cx > 0) {
cx--;
scol = String((char)('A' + (cx % 26))) + scol;
cx = cx / 26;
}
list->Add(scol);
}
StringGrid1->Rows[0]->Assign(list);

pfxl.Close();
delete list;

DWORD dw9 = ::GetTickCount();
RichEdit1->Lines->Add(String("経過時間 = ") + (dw9 - dw0));
RichEdit1->Perform(EM_SCROLL, SB_LINEDOWN, 0);
}

注1

Button3のExcelのクラスの例との違いは、変数pfxlのクラスがCPfOOCalcであることだけです。


ソースファイル

サンプルプログラムはC++Builder 2007で作成しました。サンプルプログラムのソースを以下のファイルにまとめました。

excelread.zip


プロジェクト名は ExcelRead.cbproj です。
C++BuilderXE でもコンパイルできます。

ご意見、ご要望、バグ、等ございましたらメールでご連絡ください。