技術記事

HotXLSでワークシートにチャートオブジェクトを埋め込む

HotXLSは、チャートを専用のチャートシートに置く代わりに、セル範囲にアンカーしてワークシート上へ直接置けます。BIFF8の言葉で言えば、タイプ5のOBJレコードを持つ描画シェイプを書き、チャートサブストリームをシートのレコードストリーム末尾へ停めておく、ということです。これはExcelが生成するレイアウトそのままであり、リーダーが探しに行く場所そのままでもあります

この区別が効くのは、運用レポートを生成する人すべてです。チャートシートは、1枚の見出しビジュアルには良い住まいです。月次の地域別内訳が望むのは、要約する数字の隣、同じシート上、属するセルブロックのサイズに合わせたチャートです。読み手はタブを切り替えて文脈を失う代わりに、1度スクロールするだけで済みます

読みは既にあり、書きは無かった

この非対称には名前を付ける価値があります。作業の形を決めるからです。HotXLSは埋め込みチャートを既に読めました。ワークシートのレコードストリームにチャートサブストリームと印を付けられたBOFが含まれていると、パーサはコンテキストを切り替え、チャートレコードを収集し、閉じのEOFで、OBJレコードが導入した描画シェイプへ引き渡します。この経路は、ライブラリが開いたことのあるすべてのExcel作成ブックが通ってきた道です

欠けていたのはオーサリング側で、有用な帰結として、新しいライタには狙うべき精密な仕様がありました。既存のリーダーが既に再接続するバイトレイアウトを生み出すことです。バイナリフォーマットの機能に対して、自分が変更する権利のない、独立に書かれたリーダー以上の受け入れ基準はありません

埋め込みチャートを構成するもの

揃わなければならない部品は3つです。描画レイヤーがホストコントロールのシェイプを出し、オブジェクトレイヤーが、共通オブジェクトデータでオブジェクトタイプ5を宣言するOBJレコードを出し、レコードストリームがチャートサブストリームそのものを出します。OBJレコードのオプションフラグは、Excelがチャートフレームに書くもの、配置済み、ロック済み、自動線、自動塗りで、ユーザーがクリックしたときに埋め込みチャートがネイティブのそれのように振る舞うのはこのためです

HotXLS が BIFF8 チャートサブストリームを Delphi のワークシートへアンカーする仕組み。揃う 3 部品として、描画レイヤーのホストコントロールシェイプ、共通オブジェクトデータでオブジェクトタイプ 5 を宣言する OBJ レコード、シートレコードストリーム末尾に停められたチャートレコードチェーン。チャート BOF がパーサのコンテキストを切り替え、閉じの EOF がレコードを再接続する
3つのレイヤーが1つの埋め込みチャートを運びます。描画シェイプがアンカーし、OBJレコードがチャートホストとして型を与え、シートストリーム末尾のチャートサブストリームが、リーダーが再接続するレコードを供給します

アンカーには注を付ける価値があります。オフバイワンバグのよくある源だからです。HotXLSのAPIは、ライブラリの他の部分と同じく、1起点の行番号と列番号を受け取ります。ファイルに書き込まれるクライアントアンカーはゼロ起点です。変換はAddChartObjectの内部で起きるため、呼び出し側はどこかで使っているのと同じ座標系に留まれます。ただし自分の呼び出しと16進ダンプを突き合わせる人は、その境界のどちら側を読んでいるのかを覚えておく必要があります

var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Series: array[0..1] of TXLSChartSeriesInfo;
begin
  Book := TXLSWorkbook.Create(nil);
  try
    Book.LoadFromFile('regional-sales.xls');
    Sheet := Book.Sheets[0];

    FillChar(Series, SizeOf(Series), 0);
    Series[0].Name := 'Actual';
    Series[0].Categories := 'Data!$A$2:$A$13';
    Series[0].Values := 'Data!$B$2:$B$13';
    Series[0].DataLabels.ShowValue := True;
    Series[0].HasDataLabels := True;

    Series[1].Name := 'Target';
    Series[1].Categories := 'Data!$A$2:$A$13';
    Series[1].Values := 'Data!$C$2:$C$13';
    Series[1].SecondaryAxis := True;

    // このシートの E2:M20 にアンカー、1 起点
    Sheet.AddChartObject(xlsChartTypeColumn, 'Regional sales',
      'Month', 'Amount', Series, 2, 5, 20, 13);

    Book.SaveToFile('regional-sales-charted.xls');
  finally
    Book.Free;
  end;
end;

系列配列へのFillCharは飾りではありません。TXLSChartSeriesInfoは複数のオプションのサブレコード、データラベル、系列ごとのスタイル、近似曲線、誤差範囲を運び、それぞれがブールでゲートされています。スタック上の部分的に初期化されたレコードは、誰もセットしていないフラグをエミッタへ渡します。配列をゼロで埋め、その上で意図したフィールドをセットしてください

埋め込み経路が受け付ける系列参照

同じワークブック内の素のA1形式範囲です。この制限は見落としではなく意図的なものです。すべての参照はワークブックのシートリストに対して解決され、チャートレコードが要する外部参照インデックスへ変換されます。名前付き範囲や外部ワークブック参照は、長さゼロの解析済み式を持つプレースホルダへフォールバックします。チャートはきれいに書き上がりますが、その特定の系列は、範囲を指し直すまでデータソースを持ちません

埋め込み BIFF8 チャート経路における HotXLS の系列参照の受け付け。同一ワークブック内の Data!$B$2:$B$13 のような A1 形式範囲はシートリストに対して解決され、チャートレコードが要する外部参照インデックスへ変換される一方、名前付き範囲と外部ワークブック参照は長さゼロの解析済み式を持つプレースホルダへフォールバックする。両方とも AddChartSheet が対応
同一ワークブック内の素のA1形式範囲だけがチャート系列参照へコンパイルされます。それ以外は指し直されるまでプレースホルダとしてきれいに書き上がり、完全な経路はAddChartSheetにあります

理由は素直な工学的トレードです。完全な参照コンパイル経路はチャートシートルート上に、ワークシートコレクション層の内側に包まれて存在しており、これをきれいに引き剥がすことは、実務ではまれなケースのために100行の解決ロジックを複製することを意味します。埋め込みチャートはほぼ常に、自分のシートか兄弟データシートのセルをプロットします。名前付き参照と外部参照はAddChartSheet経由でチャートシート経路が対応するため、使えないものは何もなく、入り口が違うだけです

系列モデルのそれ以外は、両ルートで同一に振る舞います。副軸のバインド、系列ごとの線、塗り、マーカースタイル、近似曲線、誤差範囲、データラベルはすべてTXLSChartSeriesInfoの一部であり、すべて同じやり方で出力されます。チャート定義は、呼び出しだけ変えて、埋め込みオブジェクトとチャートシートの間を移動できます。副軸フラグの背後にある軸グループの仕組みはBIFF書き込みにおける副軸グループで扱います

チャートタイトルが2文字に読めた原因

バイト数を期待する場所に文字数が渡されたからです。しかもBIFF Unicode文字列は、この間違いを書きやすく、見つけにくくします。ショート形式のBIFF Unicode文字列は文字数とフラグバイトで始まり、フラグバイトは、ペイロードが1文字1バイトか2バイトかを告げるhigh-byteビットを運びます。16ビットペイロードを、文字数をバイト長のように扱って読むと、手に入るのは正確に文字列の半分です。Salesという名前の系列はSaとして返り、チャートタイトルも同じように切れます。タイトルと系列ラベルはデコード経路を共有しているからです

この欠陥が際立っているのは、同じレコードファミリーの中で3回再発したことです。近似曲線名で1回、ピボットチャート名で1回、チャートタイトルで1回。発生のたびに、新機能の新しいバグに見えました。3回とも、同じ「掛け忘れ」でした。最後にこれを閉じたルールは機械的であり、判断を挟まず適用すべきものです。これらの文字列を読むときは、必ず最初にhigh-byteフラグを確認し、バッファに触れる前に文字数へペイロード幅を掛ける。レコードレベルの詳細はXLUnicodeStringの文字数とhigh-byteフラグのデコードにあります

// 埋め込みチャートは描画レイヤーを画像やシェイプと共有するため、
// シート上の既存描画は保持される。AddChartObject は作成した
// オブジェクトのインデックスを返す
var
  ObjIndex: Integer;
begin
  ObjIndex := Sheet.AddChartObject(xlsChartTypeLine, 'Trend',
    'Week', 'Units', Series, 2, 8, 18, 16);
  if ObjIndex < 0 then
    raise Exception.Create('chart object was not created');
end;

代替手段に比べた埋め込みチャートの位置

経路は3つあり、それぞれ別の問いに答えます。埋め込みチャートオブジェクトは、ワークシート上でデータの隣に属するものであり、ほとんどのレポートが望むものです。チャートシートは1枚のプレゼンテーション用ビジュアルに向き、完全な参照コンパイル経路を与えます。読み込んだファイルからの既存チャートをそのまま保持するのは、ブックがExcelから来ていて、誰もライブラリに再解釈されたくない書式を伴うときの正解です。このパススルーの挙動は保持されるChartMLと複合チャートで説明しています

埋め込みチャートは描画レイヤーに乗るため、同じシート上の画像やシェイプを置き換えるのではなく共存します。そのレイヤーの一般モデルはHotXLSにおけるチャート、画像、描画で扱います。3つの経路はすべて、HotXLS Delphiスプレッドシートコンポーネントに同梱されています。選択は、ライブラリが何を表現できるかではなく、レポートがどう見えるべきかの話です

残すべきは方法論の指摘です。バイナリフォーマットの機能に既存のリーダーがあるなら、仕様のあなたなりの読みではなく、リーダーに向かってライタを作れ、ということです。リーダーは、実アプリケーションが実際に生み出したファイルとの何年にもわたる接触を、仕様が曖昧に述べる部分まで含めて符号化しています。それを満たすライタは、Excelをも満たす可能性がはるかに高いのです