VCLからExcelにアクセス(C++Builder版)

第9回デベロッパーキャンプ セッションA2でネタになった、VCLを使ってExcelにアクセスするサンプルのC++Builder版を作ってみた。
オリジナルと違うのは読み込み元のデータベースをdbExpressでBlackfish SQLに接続したこと。 それに伴って、ソースコードに一部変更あり。

読み込み元のデータベースの作成から。

  1. データエクスプローラで[BLACKFISHSQL]を選択して、[新規接続の追加]を選択。接続名を"BSQL_EMP"にして保存。
  2. [BSQL_EMP]上で右クリックして[接続の変更]を選択。ダイアログが表示されるので、以下の項目を入力。
    • サーバー名:localhost
    • データベース名:C:/Users/Public/Documents/RAD Studio/7.0/Demos/database/databases/BlackfishSQL/employee.jds
  3. [テスト接続]をクリックして接続出来ているかどうか確認して、[OK]をクリック。
データベースファイルはデモの中に。注意して欲しいのはデータベース名のフォルダ区切りはスラッシュ。

基本的にはDelphiとあまり変わりがないのだけど、以下に注意。

  • Delphiでの"ExcelRange"といった型名は、C++Builder版では"ExcelRangePtr"という感じで"Ptr"が付く。もちろん、メソッドやプロパティへのアクセスはアロー演算子で。
  • プロパティへのアクセスは出来ない場合が多いので、その場合はアクセサ経由でアクセスする。(NumberFormat→get_NumberFormat/set_NumberFormat)
  • メソッドに値を渡す場合は原則としてVariant型。TVariantやOleVariantでもOK。当然、文字列等渡す場合はキャストしてやること。
  • xlHAlignCenterといった定数はクラス内のenumとなっているので、XlHAlign::xlHAlignCenterのようにクラス名を付ける。定数の定義はExcel_2K.hに記述されている。

以下のようなDelphiでのソースコード

procedure TForm1.SetHeaderFormatClick(Sender: TObject);
var
  col_count: Integer;
  rs: string;
  er: ExcelRange;
begin
  col_count := Table1.FieldDefs.Count;
  rs := Format('%s%d:%s%d', [GetColTitle(1), header_count, GetColTitle
      (col_count), header_count]);
  er := ExcelWorksheet1.Range[rs, EmptyParam];
  er.NumberFormat := 'G/標準'; // 標準
  er.HorizontalAlignment := xlHAlignCenter; // 中央
  er.Font.Bold := True; // 太字
  er.Font.Italic := True; // 斜体
end;

C++Builderではこんな感じになる。

void __fastcall TForm1::SetHeaderFormatClick(TObject *Sender)
{
  int col_count = Table1->FieldDefs->Count;

  Variant rs1 = Address(header_count, 1);
  Variant rs2 = Address(header_count, col_count);
  ExcelRangePtr er = ExcelWorksheet1->Range[rs1][rs2];

  er->set_NumberFormat(Variant("G/標準")); // 標準
  er->set_HorizontalAlignment(Variant(XlHAlign::xlHAlignCenter)); // 中央

  FontPtr f = er->get_Font();
  f->set_Bold(Variant(true));  // 太字
  f->set_Italic(Variant(true)); // 斜体
}

オリジナルはセルのアドレスは直接生成しているけど、C++Builder版ではAddressというユーティリティー関数で行・列からセルのアドレスを生成している。

String __fastcall TForm1::GetColTitle(int idx)
{
  String t;
  TCHAR ch;

  if (idx <= 26) {
    ch = 'A' + idx - 1;
    t = ch;
  } else {
    ch = 'A' + (idx - 1) / 26 - 1;
    t = ch;
    ch = 'A' + (idx - 1) % 26;
    t += ch;
  }
  return t;
}

String __fastcall TForm1::Address(int Row, int Col)
{
  String a = Format("%s%d", OPENARRAY(TVarRec, (GetColTitle(Col), Row)));
  return a;
}

もう一つの違いはデータのアクセス。一つ一つセルに埋めていく「低速版」。

void __fastcall TForm1::SetBodyValuesSlow()
{
  int row_count = 0;
  ExcelRangePtr er;
  Variant rs;
  Variant v;
  TField* Field;

  Table1->First();
  while (Table1->Eof == false) {
    ++row_count;
    for (int i = 0; i < Table1->FieldDefs->Count; ++i) {
      rs = Address(header_count+row_count, i + 1);
      er = ExcelWorksheet1->Range[rs];
      Field = Table1->FieldByName(Table1->FieldDefs->Items[i]->Name);

      if (i == 4) {
        v = Field->AsDateTime;
      } else if (i == 9) {
        v = Field->AsCurrency;
      } else {
        v = Field->Value;
      }
      er->set_Value(v);
    }
    Table1->Next();
  }
}

TIMESTAMP型とDECIMAL型がVariantにうまく変換できないので、AsDateTimeとAsCurrencyで取得している。
続いて、二次元Variant配列を使った「高速版」。

void __fastcall TForm1::SetBodyValuesFast()
{

// 2次元のVariant配列を作成
  int b[] = {0,  Table1->RecordCount - 1, 0, Table1->FieldDefs->Count - 1};
  Variant ar = VarArrayCreate(b, 3, varVariant);

  Table1->First();
  int row_count = 0;

  int idx[2];

  Variant fv;
  TField* Field;
  while (Table1->Eof == false) {
    for (int i = 0; i < Table1->FieldDefs->Count; ++i) {

      Field = Table1->FieldByName(Table1->FieldDefs->Items[i]->Name);
      if (i == 4) {
        fv = Field->AsDateTime;
      } else if (i == 9) {
        fv = Field->AsCurrency;
      } else {
        fv = Field->Value;
      }

      // Variant配列にフィールドの値をセット
      ar.PutElement(fv, row_count, i);
    }

    Table1->Next();
    ++row_count;
  }

  // エクセルの広い範囲に一気にVariant配列を書き込む!!
  Variant  rs = Address(header_count+1, 1, header_count+row_count, Table1->FieldDefs->Count);
  ExcelRangePtr er = ExcelWorksheet1->Range[rs];
  er->set_Value2(ar);
}

二次元Variant配列の生成とアクセスが大きく変わっている。あと、なぜかSALARYフィールドの書式が反映されない。

さすがに全ソースコードは晒せないので、プロジェクト一式はここから。
プロジェクトを作り直して、Unit1.cpp/Unit1.hpp/Unit1.dfmを再指定すればC++Builder 2009でもいけるかも。