技術記事

HotXLSによるDelphiのカスタムスプレッドシートグリッド

HotXLSは、Excelをインストールしたり OLE オートメーション経由で操作したりすることなく、XLS、XLSX、XLSM、ODSのワークブックをDelphiまたはC++Builderフォーム内のインタラクティブなスプレッドシートグリッドとして描画するネイティブVCLコントロールであるTXLSWorkbookViewerを同梱している。この種のコントロールをうまく構築するということは、3つの具体的な問題を解決することを意味する:結合セルの内部に着地したマウスクリックを正しい論理セルにマッピングすること、可視ウィンドウよりはるかに大きいシートをユーザーがパンする際にスクロール位置・ヘッダー帯・セル選択を一貫させ続けること、そしてコメントマーカーやハイパーリンクセルへのクリックが実際に何をすべきかを決めることである

ほとんどのDelphiショップが編集とは無関係の理由でスプレッドシートビューアに手を伸ばす:アップロードされたワークブックがパイプラインに入る前にプレビューする監査ステーション、Microsoft Officeがデプロイメントイメージの一部ではないキオスクやレポートビューア、あるいは実際のExcelプロセスをCOM経由で自動化する予測不能性なしにワークブックの内容を表示する必要があるQAツールである。単純な文字列グリッドはセル内のテキストをすぐに手に入れられるが、スプレッドシートファイルは単純なグリッドではない:セルは基礎となるモデルの中で1回しか存在しないブロックへと結合し、シートは固定ヘッダー帯と独立した水平・垂直スクロール位置を持ち、個々のセルは独自の相互作用モデルを必要とするコメントとハイパーリンクを持つ。TXLSWorkbookViewerはそのギャップに対するHotXLSの答えであり、その内部設計は同様のコントロールをゼロから構築しようとする誰にとっても妥当な設計図である

ワークブックビューアはどうやってExcelへの依存を避けるのか

TXLSWorkbookViewerは、Excelを通じて文書を開いて操り人形のように動かすのではなく、HotXLS自身の解析済みオブジェクトモデルを通じて読むことで、Excelへの依存を完全に回避する。Workbookプロパティは従来のXLSファイル用に既存のTXLSWorkbookをバインドし、XlsxWorkbookはXLSX、XLSM、テンプレートの各バリアント用にTXLSXWorkbookをバインドする。どちらもアプリケーションの他の場所ですでに開かれていることがあり、ビューアはそこから読み取るだけである。コントロール自身がそのファイルを所有すべき場合、LoadFromFileは拡張子を調べ、XLSX、XLSM、XLTX、XLTM、ODSを現代のエンジンに、それ以外を従来のエンジンに振り分け、コントロールがクリアまたは破棄されたときに、それが作成したワークブックのどちらであれ解放する

var
  Viewer: TXLSWorkbookViewer;
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  if Book.Open('quarterly-report.xlsx') <> 1 then
    raise Exception.Create('Could not open workbook');

  Viewer := TXLSWorkbookViewer.Create(Self);
  Viewer.Parent := Self;
  Viewer.Align := alClient;
  Viewer.XlsxWorkbook := Book;        // the viewer does not take ownership
  Viewer.GoToCell(1, 1);

  Caption := Viewer.WorksheetName + ': ' + Viewer.SelectedCellText;
end;

結合範囲の内部で正しいセルを見つける

TXLSWorkbookViewerにおいてクリックを正しいセルに解決することは2段階のルックアップであり、この分割が重要なのは、ピクセルの幾何学とスプレッドシートの意味論が本質的に異なる問題だからだ。第一段階は純粋な幾何学である:プライベートなCellAtPointメソッドは、クリックされたX・Y座標を含む帯を見つけるまで現在のスクロール位置から列幅と行高を歩き、結合セルについての認識は一切持たない。第二段階は意味論的である:選択を変更するあらゆる経路——マウスクリック、矢印キー、Tab、あるいはGoToCellへの直接呼び出し——は1つの内部のChangeSelectionルーチンに流れ込み、これは選択が実際に変わる前に、生の行と列をどんな結合に対しても正規化し、その結合のアンカーセルにスナップさせる

アンカーは結合範囲の左上セルであり、それはそのブロック内で、基礎となるワークブックモデルの中で値・書式・コメント・ハイパーリンクを本当に保持している唯一のセルである。その結合が視覚的にカバーする他のすべてのセルは、データ自体としては空である。従来のXLSワークブックの場合、アンカーはCell.MergeAreaから来る。これは所有セルを指すRowColumnを持つIXLSRangeである。XLSXとODSのワークブックの場合、MergedCells.FindAtが同じアンカーをRow1Col1として公開するTXLSXMergedRangeを返す。描画は、結合セルの矩形をその完全な行・列スパンへ拡張し、そのスパン内のセルをスキップすることで、同等の問題を独立して解決する。そのため選択の輪郭は、そのアンカーの角だけではなく結合ブロック全体を包む。結合レイアウトを読み戻すだけでなく書き込むことは、レポートテンプレート向け結合セルレイアウトの関連記事で扱われている、関連するが別個の問題である

var
  Sheet: TXLSXWorksheet;
begin
  Sheet := Book.Sheets.Add('Summary');
  Sheet.MergeCells(2, 2, 3, 4);       // B2:D3
  Sheet.Cells[2, 2].Value := 'Region totals';

  Viewer.XlsxWorkbook := Book;
  Viewer.GoToCell(3, 4);              // targets the bottom-right corner of the merge
  // SelectedRow is now 2 and SelectedCol is now 2: normalized to the anchor cell
end;

何がスクロール・ヘッダー・選択を同期させ続けるのか

TXLSWorkbookViewerは3つの別個の状態を一貫させ続ける:TopRowLeftColに保持される論理的なスクロール位置、CreateParamsの中でWS_HSCROLLWS_VSCROLLを通じてコントロールが要求するネイティブなWindowsスクロールバー、そしてSelectedRowSelectedColにある現在の選択である。スクロールバーをドラッグしたりマウスホイールを回したりするとWM_HSCROLLWM_VSCROLLWM_MOUSEWHEELが発火し、これらはTopRowまたはLeftColを更新して再描画する。選択は動かない、これはExcel自体がパン(視点移動)と選択を分離しているのと一致している。これらの更新のいずれかの後、UpdateScrollBarsSetScrollInfoを通じて新しい位置をネイティブのスクロールバーに押し戻すため、つまみがグリッドが実際に表示しているものと一致しなくなることは決してない

キーボードナビゲーションは反対方向で同じ同期を実行する:選択が可視グリッドの端を越えて動くとEnsureSelectionVisibleが呼ばれる。これは、行と列がカスタムサイズを持ちうるため単純に1ずつ増やすのではなく実際の列幅と行高を積算することでTopRowまたはLeftColを調整し、その後UpdateScrollBarsを呼んで、つまみがキーボードがちょうどビューをどこに動かしたかを反映するようにする。RowHeaderWidthColumnHeaderHeightを通じてサイズ設定される行番号・列文字のヘッダー帯は、TopRowLeftColが下のデータをスクロールしている間、画面上に固定されたままであるこのコントロールの部分であり、これがこのコントロールが自前で行う固定表示の範囲である:これはExcelのウィンドウ枠の固定機能ではなく、シートの残りがそれを通り過ぎてスクロールする間に任意のデータ行や列を固定する組み込みの方法はない。完全には制御できていないファイルに対してビューアを出荷する前にテストする価値のある一つの境界は、TopRowLeftColがワークシートの実際に使用されている範囲に対してクランプされていないことである。そのため構造的な限界までドラッグされたつまみは、実際にデータを保持している最後の行や列ではなく、1,048,576行目や16,384列目に着地して空白のグリッドを表示することがある。それが目立つほど大きいワークブックは通常、大規模ワークブックのパフォーマンスに関する記事で扱われている読み込み側の注意も必要とするほど大きい

コメントとハイパーリンクをマウス・選択イベントに配線する

TXLSWorkbookViewerは、コメントとハイパーリンクをホバーターゲットとしてではなく現在選択されているセルの属性として扱う。そのためSelectedCellCommentTextSelectedCellCommentAuthorSelectedCellHyperlinkは、選択がマウスクリック、矢印キー、あるいはGoToCellの呼び出しのいずれで動いても、OnSelectionChangeが発火するたびに更新される。コメント付きのセルは、Excel自身のコメントフラグに似た視覚的な合図として右上隅に小さな赤い三角形が描かれるが、そのマーカーは純粋に視覚的なものである:コントロールに組み込まれたホバートリガーのツールチップは存在しないため、選択時ではなくマウスオーバー時にポップアップが欲しいアプリケーションは、その層を自分で構築しなければならない。ハイパーリンクの起動も同じ選択優先の方法で機能する:セルをダブルクリックするとActivateSelectedCellが呼ばれ、これはSelectedCellHyperlinkを読み、それが空でなければ、ターゲットアドレスとハンドラが設定するためのvar Handled: BooleanパラメータとともにOnHyperlinkClickを発生させる

OnHyperlinkClickがしないことも同じくらい重要である:TXLSWorkbookViewerは、ハンドラがHandledをtrueに設定しようがfalseのままにしようが、決して自分でShellExecuteを呼んだりブラウザを開いたりしない。ナビゲーション、そして何が安全なターゲットとみなされるかについての判断は、完全にホストアプリケーションの責任である。これは、それが信頼された社内ツールに組み込まれているのか顧客がたった今アップロードしたファイル用のビューアなのか一切知らないコンポーネントにとって正しい既定動作である

procedure TMainForm.ViewerSelectionChange(Sender: TObject; Row, Col: Integer);
begin
  if Viewer.SelectedCellCommentText <> '' then
    StatusBar.SimpleText := Viewer.SelectedCellCommentAuthor + ': ' +
      Viewer.SelectedCellCommentText
  else
    StatusBar.SimpleText := Viewer.SelectedCellHyperlink;
end;

procedure TMainForm.ViewerHyperlinkClick(Sender: TObject;
  const Target: WideString; var Handled: Boolean);
begin
  ShellExecute(0, 'open', PWideChar(Target), nil, nil, SW_SHOWNORMAL);
  Handled := True;
end;

選択範囲とキーボードナビゲーションの限界

TXLSWorkbookViewerにおける選択は常に単一の論理セルであり、SelectedRowSelectedColとして追跡される。基本コントロールには矩形の複数セル範囲選択は存在しない。そのため、セルのブロックに対して作用する必要のある機能は、選択オブジェクトから読み取るのではなくその上に構築する必要がある。キーボードのカバー範囲は意図的に基本的なものにとどまっている:矢印キーは一度に1セルずつ移動し、Homeは行の先頭へ、あるいはCtrlと組み合わせるとA1セルへ戻り、Page UpとPage Downは10行ジャンプし、TabとShift+Tabは列を移動する。データ領域の端へのCtrl+矢印ジャンプもShiftによる範囲選択の拡張もない。そのためExcelから直接来たユーザーは密度の高いシートでこのギャップに気づくだろう

列の限界は、結合の正規化を扱うのと同じChangeSelectionのボトルネックで強制され、それは意図的にエンジンごとに異なる:従来のTXLSWorkbookにバインドされたビューアは、BIFF8形式の構造的な上限である256列目でクランプされ、TXLSXWorkbookにバインドされたビューアは、Excel 2007以降からXLSXが引き継いだ現代の16,384列の上限を尊重する。行はどちらの場合も1,048,576行に上限が設けられている。そのため、同じビューアでレガシーなXLSファイルとXLSXファイルを開くことの実際的な違いは、完全にグリッドがどこまで右に行くことを許すかに関するものである

これは、ピクセルのルックアップ、アンカーの正規化、そして一握りのメッセージハンドラに分解してしまえば、それほど風変わりなものではない。しかし実際のファイル、実際の結合、コメント、ハイパーリンクの下でこの3つを一致させることが、この種のコンポーネントにおける作業の大部分である。TXLSWorkbookViewerは、それが描画する元となる従来型・XLSXオブジェクトモデルとともに、DelphiおよびC++Builder向け標準HotXLS Excelコンポーネントの一部として出荷される