技術記事

DelphiとHotXLSを使用したExcelファイルでのスレッドコメント

Delphiからワークブックを生成し、従来のコメントAPIを使用してセルに注釈を追加すると、Excel 365はそのテキストを「メモ」の下に分類します。これは、返信フィールドも、作成者のアバターも、解決ボタンもない古いスタイルの黄色いボックスです。HotXLSは、XLSXエンジンの AddThreadedComment および AddThreadedReply を介して、本物のExcel 365スレッドコメントを書き出します。シートごとに専用の xl/threadedComments/threadedCommentN.xml パーツを出力し、すべての作成者をブックレベルの person(人物)パーツに登録するため、コードが書き出す会話は、レビューアがExcel内で返信するオブジェクトと同じものになります。

この違いは重要です。なぜなら、これら2つの機能は見た目が似ているだけだからです。「メモ」は単に浮かぶテキストボックスですが、「スレッドコメント」は、固定された識別子、返信ツリー、作成者レジストリ、および解決(Resolved)フラグを備えた会話の記録であり、これらはレビューワークフローが実際に必要とする要素です。この記事では、まずOOXMLの構造について順を追って解説します。これら2つの保存モデルを並べて見れば、Excel内での動作の違いに疑問を持つことはなくなるでしょう。

Excel 365でコメントがメモとして表示される理由

端的に言えば、2つの世代のコメント機能が1つのグリッドを共有しており、それらはパッケージ内の全く異なる場所に保存されているからです。AddComment が対象とするレガシーなモデルは、テキストを xl/comments1.xml (コンテンツタイプ application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml)に保存し、吹き出しの位置は付随するVML描画パーツである xl/drawings/vmlDrawing1.vml によって制御します。このペアはリボンの登場よりも古いものであり、Excel 365は今でもこれを忠実に読み取りますが、結果を「メモ」としてレンダリングし、スレッド化するためのUIを提供しません。なぜなら、保存データの中にスレッド化のための情報が一切含まれていないからです。作成者は単なる表示用の文字列であり、位置はVMLの図形である、それがこのモデルのすべてです。

Excel 365で導入されたスレッドコメントは、VMLレイヤーを完全に無視します。会話を含む各シートは、コンテンツタイプ application/vnd.openxmlformats-officedocument.spreadsheetml.threadedComments+xml の下に独自の xl/threadedComments/threadedCommentN.xml パーツを保持し、ワークブックはすべての参加者を一度だけ一覧表示する単一の xl/person/person.xml パーツ(コンテンツタイプ ...spreadsheetml.people+xml)を保持します。HotXLSは、同じワークシート上にこれら両方のモデルを公開しています。AddComment はレガシーなペアを書き出し続け、AddThreadedComment は新しいパーツを書き出します。もし出力結果がメモとして表示されているなら、呼び出したAPIが前者であり、求めていたのが後者であったということです。レガシーモデルは、今でも一部のタスクには適した手段です(XLS側を含むこれについての詳細は、姉妹記事のレビューワークフローにおけるセルのコメントとハイパーリンクで詳しく説明されています)が、それをスレッドにアップグレードすることはできません。

HotXLSがパッケージに書き出す内容

HotXLSは、各返信を TXLSXThreadedComment オブジェクトとしてモデル化します。これには、セルのアンカー(Ref)、GUID形式の識別子(Id)、オプションの ParentId、人物リストを介して解決される AuthorId、表示名のミラー(DisplayName)、テキストの本文、解決フラグ(Done)、および ISO-8601 形式の DateTime が含まれます。保存時、各エントリは <threadedComment> 要素になり、t 属性が識別子、dT 属性がタイムスタンプを保持し、子返信にのみ存在する parentT 属性が親の識別子を指定します。Excelは、parentTt を一致させることで会話のツリーを再構築します。XML内にはネスト構造はなく、参照情報を伴うフラットなリストとして保存されます。

人物パーツ(Person Part)は、手動で実装しようとする人の多くが忘れがちな部分です。スレッドコメント内のすべての authorId は、ワークブックの関係性に紐付けられ、独自のコンテンツタイプの上書き設定を持つ xl/person/person.xml 内の <person> エントリに解決されなければなりません。そうしないと、Excelは表示名を表示できません。HotXLSはこの登録を自動的に処理します。AddThreadedComment は表示名に基づいてワークブックの Persons リストから作成者を検索し、存在しない場合は新しいGUIDを持つエントリを作成します。これにより、同じ作成者による連続したコメントが1つのIDを共有します。ワークシートの関係性には threadedComments の関係性が追加され、ワークブックの関係性には person の関係性が追加され、さらにコンテンツタイプ・マニフェストには両方の上書き設定が追加されます。これらはコード上に現れることなく、すべて自動的に行われます。

AddThreadedCommentとAddThreadedReplyを使用した返信チェーンの構築

AddThreadedComment は会話のルート(起点)を作成します。その ParentId は空のままになり、これがスレッドの最上部であることを示します。AddThreadedReplyParentRef パラメータ(ルートが存在するセルの参照)を受け取り、そのアンカーにあるルートを検索し、新しい返信の ParentId にそのルートの Id をスタンプします。どちらも新しい TXLSXThreadedComment を返すため、作成したオブジェクトに対してタイムスタンプや解決状態を設定できます。

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Root: TXLSXThreadedComment;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('forecast.xlsx');
    Sheet := Book.Sheets[0];

    // Conversation root; ParentId stays empty
    Root := Sheet.AddThreadedComment(8, 3,
      'Q3 figure looks low against the pipeline export', 'Maria Ortiz');
    Root.DateTime := '2026-07-06T09:12:00Z';

    // Replies anchor at the same cell; ParentRef names the root cell
    Sheet.AddThreadedReply(8, 3, Root.Ref,
      'Pipeline export missed the EMEA renewals, re-running', 'Jan Kowalski');
    Sheet.AddThreadedReply(8, 3, Root.Ref,
      'Confirmed, refreshed figure lands tomorrow', 'Maria Ortiz');

    Book.SaveAs('forecast-reviewed.xlsx');
  finally
    Book.Free;
  end;
end;

上記のリストにある2つの詳細に注目してください。第一に、手動で組み立てた文字列ではなく Root.Ref を渡しています。ライブラリが生成した参照は、AddThreadedReply が検索する値と一致することが保証されるため、アンカー指定のズレ(オフバイワンエラー)によるバグを完全に回避できます。第二に、両方の作成者がプレーンな表示名として渡されています。人物レジストリ、GUIDによるアイデンティティ、および authorId の紐付けはすべて呼び出しの裏側で行われました。保存されたファイルをExcel 365で開けば、セルへのアンカー、返信順序、および2人の異なる参加者がすべてライブな状態で表示されます。返信(Reply)をクリックすれば、Excelはコードが開始したのと同じスレッドに返信を追加します。

会話の読み戻し:往復処理(ラウンドトリップ)の動作

HotXLSはバージョン 2.122.0 以降、スレッドコメントのラウンドトリップに対応しています。保存されたパッケージを再び開くと、各シートの会話リストとワークブックの参加者リストがパーツから再構築されるため、識別子、返信リンク、および表示名が「開いて保存する」サイクルで完全に維持されます。リーダーはシートコンテンツの前に person.xml を解析するため、読み込み時にすべての authorId が表示名に解決されます。解析された人物識別子は再生成されずそのまま維持されるため、コードとExcelの間でワークブックが何度も往復しても、作成者のアイデンティティは安定して維持されます。

var
  i: Integer;
  Cmt: TXLSXThreadedComment;
begin
  Book.Open('forecast-reviewed.xlsx');
  Sheet := Book.Sheets[0];
  for i := 0 to Sheet.ThreadedComments.Count - 1 do
  begin
    Cmt := Sheet.ThreadedComments[i];
    if Cmt.ParentId = '' then
      Writeln('Root at ', Cmt.Ref, ' by ', Cmt.DisplayName, ': ', Cmt.Text)
    else
      Writeln('  reply by ', Cmt.DisplayName, ': ', Cmt.Text);
  end;
  Writeln('Participants: ', Book.Persons.Count);
end;

フラットなリストと参照による保存の仕組みがここに現れています。ThreadedComments はパーツ内の順序で返信を列挙し、空の ParentId によって各ルートが識別されます。もしツリー構造が必要な場合(例えばレビューログをエクスポートするなど)は、まず Ref でグループ化し、各セルのグループ内で ParentIdId にチェーン(連結)します。この「解析したものをそのまま保存する」動作は、ライブラリ内のより広範な方針の一例です。これと同じ原則がパッケージの他の部分にどのように適用されるかについては、姉妹記事のテーマ、拡張リスト、および計算チェーンのロスレス往復(ラウンドトリップ)で詳しく説明されています。

解決状態、タイムスタンプ、および作成者のアイデンティティ

3つの属性がレビューのセマンティクスを保持しており、これらいずれも返されたオブジェクト上で書き込み可能です。Donedone 属性にマッピングされ、Excelでは解決されたスレッドは「元に戻す」リンク付きで折りたたまれて表示されます。DateTime は ISO-8601 準拠の UTC 作成日時(dT)です。HotXLSは、空のままにされた場合はパーツの検証が通るように固定のプレースホルダーを代わりに書き出しますが、実際の日時を設定する方がスレッドの履歴が読みやすくなります。アイデンティティ側では、各 TXLSXPersonProviderId (ローカルに作成されたエントリの場合は通常 None)および、アプリケーションがそれを把握している場合にUPNやメールアドレスを設定できる UserId を公開しています。

var
  Root: TXLSXThreadedComment;
  Person: TXLSXPerson;
begin
  Root := Sheet.ThreadedComments.FindAt('D9');
  if Root <> nil then
    Root.Done := True;   // saved as done="1"; Excel shows the thread resolved

  Person := Book.Persons.FindByDisplayName('Maria Ortiz');
  if Person <> nil then
    Person.UserId := 'maria.ortiz@example.com';
end;

率直な制限として、HotXLSは <mentions> 要素を空のまま書き出します。@メンションの記録(Microsoft 365で同僚の名前をコメントテキスト内で強調表示し、通知をトリガーする仕組み)はモデル化されていないため、@記号を含むテキストは単なるプレーンテキストとして保存されます。生成されたレビュー用ワークブックでは、これが損失になることは滅多にありませんが、ワークフローがメンション通知に依存している場合は、人間がExcel内でそれを追加するように設計してください。人物パーツ内の作成者IDは、ワークブックレベルのメタデータと相性がよく、これらは、生成されたファイルの監査証跡が記録されるワークブックのメタデータとドキュメントプロパティに関する記事で説明されています。

古いExcelバージョンにおけるスレッドコメントの動作

Excel 2016 以前のバージョンはスレッドモデルが登場する前にリリースされたため、理解できないパーツを単に無視します。Excel 365自身がスレッドでの会話を保存する際、古いバージョンでもセルの注釈として何かが表示されるように、「[スレッドコメント]…」と書かれたレガシーコメントのシャドウ(ダミーメモ)も同時に書き出します。HotXLSはスレッド用のパーツのみを出力し、レガシーシャドウは書き出さないため、コードが作成したファイルを Excel 2016 で開いた場合はコメントのインジケーター自体が表示されません。古いバージョンが混在する環境で注釈をどうしても可視にしたい場合は、レガシーな AddComment APIが依然として適合する手段です。ただし、リーダーが1つのセルに存在するメモとスレッドをどのように調整するかはリーダー側の仕様に依存するため、十分に検証することなく同じセルに両方のモデルをスタックすることは避けてください。

もう1つの明確な制限は、ファイルフォーマット自体です。スレッドコメントはOOXML의の構造であり、BIFF8に対応する仕様はないため、.xls 側のHotXLSにはスレッド用のAPIはありません。レガシーなバイナリ形式を維持しなければならないワークブックは、古典的なメモに限定されます。実質的な判断基準はシンプルです。Excel 365やWeb・モバイル版Excel向けに .xlsx を書き出す場合は、AddThreadedComment を使用して本物のスレッド会話を作成します。Excel 2016 や .xls を対象とする場合は、AddComment を使用してメモモデルを受け入れます。受け取ったファイルを監査する場合は、両方の世界を経由したワークブックに両方のデータが混在し得るため、ThreadedCommentsComments の両方をチェックします。両世代に対応する完全なAPIは、HotXLS Delphi Excel Componentの製品ページに掲載されています。