技術記事

HotXLS Delphi Component: Delphi での protection, page setup, and printing

ワークシートの設定のうち 3 つのグループは、セルの値そのものとは無関係で、ファイルが自分のコードを離れた後にどう振る舞うかにすべて関わっている。シート保護は、ワークブックを引き渡した後でユーザーがどのセルを編集できるかを決める。ページ設定は、向き、用紙サイズ、余白を固定する。印刷設定(タイトル行の繰り返し、拡大縮小、手動の改ページ)は、任意の長さのグリッドが紙の上にどう収まるかを制御する。この 3 つはどれも、ビューアでデータを目視しただけでは表面化せず、間違っていればどれも現場で静かに壊れる。ネイティブな Delphi・C++Builder 向けスプレッドシートライブラリである HotXLS は、.xls と .xlsx に対してその表面全体を公開しており、それはつまり、その表面に組み込まれた直感に反する Excel のルールのすべてもそのまま再現するということでもある

そのルールの最初の 1 つは、生成したシートを初めて保護しようとしたときに、ほとんど誰もがつまずくものである。Protect を呼び出した瞬間、ワークブックを組み立てるときに前提としていた入力列を含め、誰もどのセルにも入力できなくなる。自分のコードはそれらの列に一切触れていないのに、まさにそれこそが原因である

すべてのセルはロック状態で生まれる

ECMA-376 は locked を保護そのもののプロパティとしてではなく、セルの書式レコードの一部として定義しており、既定値は true である。シート保護は、そのフラグを実効化するスイッチにすぎない。したがってグリッド全体は、存在した瞬間からロックフラグを休眠状態のまま帯びており、Protect の呼び出しはそれらすべてを一斉に有効化する。対処法は、順序を意図的に決めておくことである。レイアウトを組み立て、ユーザーが編集すべき範囲を明示的にロック解除し、最後に保護をかける

Delphi における HotXLS 保護順序の図。全セルは locked true で生まれ、入力範囲がまず SetLocked でロック解除され、最後に呼ばれる Sheet.Protect がロック解除セルの編集可能性を保つ
セルはデフォルトでロックされた状態で来るため、入力範囲を先にロック解除し、Protect は最後に呼んで編集可能を保ちます
Book := TXLSXWorkbook.Create;
try
  Sheet := Book.Sheets.Add('Timesheet');
  // ... ヘッダー行、名前列、およびレート数式がここに書き込まれる ...
  Sheet.Range['B2:B50'].SetLocked(False);         // ここに職種の時間入力
  Sheet.Range['F2:F50'].SetFormulaHidden(True);   // レート計算を非公開に保つ
  Sheet.Protect('review-2026');                   // これでロックフラグが有効化される
  Book.SaveAs('timesheet.xlsx');
finally
  Book.Free;
end;

SetFormulaHidden は、見落としやすい別のことを行う。保護が有効な間、セルは計算済みの値を表示し続けるが、数式バーには何も表示されない。これは、数式の中に請求レート、利益率、採点の重みづけなど、合計をクリックしたすべての受け取り手には渡したくない情報が埋め込まれている場合に重要になる。XLS ファサードでも同じ意図を、範囲ごとの IXLSRange.LockedFormulaHidden で表現できる。そちらのワークシートには 15 個の Allow* フラグ(AllowSortAllowAutoFilterAllowFormatCells など)も用意されており、保護されたシートでも封印された展示物のように固まってしまうのではなく、並べ替えやフィルターを使い続けられる

保護パスワードが実際に守っているもの

両方の形式とも、シートとワークブックの保護パスワードを 4 桁の 16 進数によるレガシーなハッシュとして保存している。16 ビットということは、任意のパスワードに衝突する文字列は無数にあり、解除ツールは一検索で見つかる。保護は、うっかりした編集を防ぐシートベルトとして扱うべきであり、アクセス制御としては扱わないこと。レビュー担当者が数式の列を上書きするのを止めるには正しい道具だが、「機密」という言葉が絡む用途には不適切な道具である

1 段上のレベルでは、XLSX ファサードの ProtectWorkbook がワークブックの構造をロックし、シートの追加、名前変更、削除、並べ替えを防ぐ。シート一覧そのものが、名前や位置でシートを索引付けする下流のパーサーとの契約になっている場合は、これを設定しておくこと。シートが 1 つ名前変更されるだけで、列が削除された場合と同じように、その先のインポートは確実に壊れる。XLS ファサードも同じ階層構造を、ワークブックレベルの TXLSWorkbook.Protect とシートごとの Protect 呼び出し、そして何かを変更する前に既存のファイルを検査する必要があるコード向けの isProtected プロパティで映し出している

要件が本物の機密性である場合、仕組みはまったく別のものに変わる。SaveAsEncrypted は、ECMA-376 の Standard Encryption 方式に基づく AES 暗号化パッケージを生成する。これはAES 保護された XLSX 出力ガイドで詳しく扱っており、レガシーな XLS ファサードも EncryptionPasswordOpen のパスワード付きオーバーロードを通じて RC4 暗号化された .xls ファイルの読み書きに対応している。この違いは机上の話ではない。保護されたシートは平文のまま流通するため、どんな zip ツールでもそのセル値を読み取れるが、暗号化されたパッケージはパスワードなしでは読めない。「給与ファイルは保護しなければならない」という監査項目は、使われている語彙が何であれ、ほとんどの場合は暗号化を意味している

HotXLS シート保護(16 ビットレガシーハッシュを格納し、セル値を任意の zip ツールが読める平文のまま残す)と、パスワードなしでは読めないままの SaveAsEncrypted AES 出力の対比図
シート保護は、平文の値が読めるままの状態での、うっかり編集に対するシートベルトです。内容を隠せるのは AES 暗号化だけです

ページ設定も文書の契約の一部である

印刷の挙動は画面上では見えないため、壊れたまま出荷されることがとても多い。顧客がワークブックを印刷した瞬間、あるいは監査人向けに PDF へエクスポートした瞬間、余白、拡大縮小、繰り返しタイトルは、誰もテストしていなかった機能要件に姿を変える。XLSX ファサードでは、これらの設定はワークシートに直接ぶら下がっている

Sheet.PageLandscape := True;
Sheet.PaperSize := xlsxPaperA4;
Sheet.SetPageMargins(0.5, 0.5, 0.75, 0.75, 0.3, 0.3);
Sheet.CenterHeader := 'Monthly Timesheet';
Sheet.RightFooter := 'Page &P of &N';
Sheet.PrintArea := '$A$1:$F$60';     // 単独の参照: シート名はここにはない
Sheet.PrintTitleRows := '$1:$1';     // ヘッダー行が各ページで繰り返される
Sheet.FitToWidth := 1;
Sheet.FitToHeight := 0;              // データが成長するにつれて下方向に伸びる
Sheet.PrintGridlines := False;

このうち 2 行には落とし穴が隠れている。ヘッダーとフッターの文字列は Excel の書式コードを使う。&P は現在のページ、&N は総ページ数を表し、&L&C&R で左・中央・右の 3 つのセクションを明示的に指定できる。もう 1 つの落とし穴は PrintArea であり、これは意図的に単独のセル参照を受け取る。HotXLS はこれをシート名なしで保存し、ファイルを書き出す際にシート名を前置する。そのため、自分で 'Timesheet!$A$1:$F$60' のように渡すと、二重に修飾された不正な参照になってしまう。同じ注意はもう 1 段下でも当てはまる。印刷範囲と印刷タイトルは、組み込みの定義名である _xlnm.Print_Area_xlnm.Print_Titles として永続化されるため、DefinedNames を通じて自分で _xlnm.* のエントリを追加してはならない。そうしなければ、2 つの仕組みが同じ場所を取り合うことになる

本番データ量に耐える拡大縮小

FitToWidth := 1FitToHeight := 0 の組み合わせは、「列は常に 1 ページに収め、データが必要とするだけ下方向にページを増やす」という意味になり、行数が変動するレポートにとって正しい既定値である。落とし穴は、30 行のテストファイルに対して固定のパーセンテージや fit-to-page の組を調整してしまうことである。同じ設定に本番の 600 行を流し込むと、出力は何十ページもの切り詰められたページに爆発するか、判読できないほど縮小されるかのどちらかになる。幅だけを拡大縮小し、長さは伸びるに任せ、PrintTitleRows でヘッダー行を繰り返せば、17 ページ目だけを見ても内容が読み取れる状態を保てる

Delphi における HotXLS 印刷スケーリングの図。FitToWidth を 1 にして全ページを 1 シート幅に保ち、FitToHeight を 0 にしてページが下方向へ増え、PrintTitleRows がヘッダーバンドを繰り返し、ClearAllPageBreaks の後に改ページを再生成
FitToWidth 1 と FitToHeight 0 はどのページも 1 シート幅に保ち、繰り返されるタイトル行と再生成された改ページが読みやすさを守ります

手動の改ページも、生成されたワークブックのほかのすべてと同じ再生成の規律に従う。AddRowBreak(BeforeRow) はセクションの境界の前で新しいページを開始するが、ジェネレーターが再実行されて行がずれると、古い改ページが表の途中に着地してしまう。まず ClearAllPageBreaks を呼び、古い位置を修正するのではなく、ジェネレーター自身の行カウンターから計算し直した改ページを追加すること。XLS ファサードでも、同等の制御は Sheet.PageSetup(向き、用紙サイズ、余白、ヘッダーとフッターの文字列、fit-to-pages)にあり、RepeatRowsRepeatColumns が印刷タイトルを担う

顧客より先に結果を確認する

保護と印刷のバグには 1 つの共通点がある。手作業で確認するのは簡単なのに、ほとんど確認されないことである。生成したファイルを Excel で開き、90 秒だけかけてみること。入力用のセルに何か入力してキー入力が受け付けられることを確認し、ロックされたセルに入力して保護のプロンプトが現れることを確認し、非表示にした数式では数式バーが空欄のままであることを確認する。次に、30 行のサンプルではなく本番規模のデータセットに対して印刷プレビューを実行し、ページ数、繰り返しタイトル行、フッターの番号付けを読み取る。このプレビューの手順は、それだけの価値がある。印刷のジオメトリは画面上に何も描画されない設定に依存しており、実物のプリンターを使わない限り、拡大縮小の誤りが見える形になるのはこの場所しかないからである

最後の設定がこのレビューを締めくくる。FreezePane(ACol, ARow) は、レビュー担当者がスクロールしている間もヘッダーブロックを表示し続ける。これは印刷の挙動ではなく画面上の挙動だが、レビュー担当者は成果物全体を一度に判断するものである。そして、デザイナーが管理するレイアウトとして最初から作られたワークブックであれば、この大部分は無料で手に入る。テンプレートレポート生成のワークフローは、ページ設定をテンプレート側に置いたままにする。そこでは人間が実物のプリンターに合わせて調整しており、コード側はデータを埋め込み、レイアウトが固まった段階で保護を再適用するだけでよい

HotXLS は Delphi と C++Builder 向けのネイティブ Object Pascal スプレッドシートライブラリである。保護とページ設定の完全な API リファレンスはHotXLS Delphi Component 製品ページに掲載している