Bài viết Kỹ thuật

Threaded Comment trong File Excel với Delphi và HotXLS

Tạo một workbook từ Delphi, chú thích một ô bằng API comment cổ điển, và Excel 365 sẽ xếp văn bản của bạn dưới dạng Notes (Ghi chú): một hộp màu vàng kiểu cũ không có trường Trả lời (Reply), không có avatar tác giả, không có nút giải quyết (resolve). HotXLS ghi các threaded comment (bình luận dạng chuỗi) thực sự của Excel 365 thông qua AddThreadedCommentAddThreadedReply trên engine XLSX, phát ra phần chuyên dụng xl/threadedComments/threadedCommentN.xml trên mỗi sheet và đăng ký mọi tác giả trong phần person ở cấp độ workbook, nhờ đó cuộc hội thoại mà code của bạn ghi là cùng một đối tượng mà người đánh giá sẽ trả lời trong Excel

Sự phân biệt này rất quan trọng vì hai tính năng này chỉ trông giống nhau. Một note chỉ là một hộp văn bản nổi; một threaded comment là một bản ghi hội thoại với các mã định danh ổn định, một cây trả lời, một danh sách tác giả và một cờ giải quyết — những thứ thực sự cần thiết cho một quy trình đánh giá. Bài viết này sẽ hướng dẫn cấu trúc OOXML trước tiên, bởi vì một khi bạn nhìn thấy hai mô hình lưu trữ này cạnh nhau, mọi sự khác biệt về hành vi trong Excel sẽ không còn là bí ẩn nữa

Tại sao các bình luận của tôi lại hiển thị dưới dạng note trong Excel 365?

Câu giải thích ngắn gọn: hai thế hệ bình luận chia sẻ cùng một lưới bảng tính, và chúng nằm ở các phần hoàn toàn khác nhau của package. Mô hình cũ — mô hình mà AddComment hướng tới — lưu trữ văn bản trong xl/comments1.xml (content type application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml) và định vị bong bóng thoại thông qua một phần vẽ VML đi kèm, xl/drawings/vmlDrawing1.vml. Cặp đó đã có trước khi có thanh công cụ ribbon; Excel 365 still reads it faithfully, nhưng nó hiển thị kết quả dưới dạng một "note" và không cung cấp giao diện trả lời, vì không có thông tin cấu trúc nào trong bộ nhớ để tạo luồng. Tác giả chỉ là một chuỗi hiển thị, vị trí là một hình dạng VML, và đó là toàn bộ mô hình

Threaded comment, được giới thiệu cùng Excel 365, hoàn toàn bỏ qua lớp VML. Mỗi sheet có cuộc hội thoại sẽ mang phần xl/threadedComments/threadedCommentN.xml riêng của nó dưới content type application/vnd.openxmlformats-officedocument.spreadsheetml.threadedComments+xml, và workbook mang một phần xl/person/person.xml duy nhất (content type ...spreadsheetml.people+xml) liệt kê mỗi người tham gia một lần. HotXLS cung cấp cả hai mô hình trên cùng một worksheet: AddComment tiếp tục viết cặp cũ, AddThreadedComment viết các phần mới. Nếu đầu ra của bạn hiển thị dưới dạng note, nghĩa là bạn đã gọi API thứ nhất trong khi thực chất muốn gọi API thứ hai. Mô hình cũ vẫn là công cụ phù hợp cho một số công việc — bài viết trước về comment ô và siêu liên kết trong quy trình đánh giá trình bày chi tiết về nó, bao gồm cả phía XLS — nhưng không có gì trong đó có thể nâng cấp thành một chuỗi hội thoại

Những gì HotXLS ghi vào package

HotXLS mô hình hóa mỗi câu trả lời dưới dạng một đối tượng TXLSXThreadedComment mang một neo ô (Ref), một mã định danh dạng GUID (Id), một ParentId tùy chọn, một AuthorId được phân giải qua danh sách person, một bản sao DisplayName, văn bản nội dung, một cờ Done và một timestamp ISO-8601 DateTime. Khi lưu, mỗi mục trở thành một phần tử <threadedComment> có thuộc tính t giữ mã định danh, thuộc tính dT giữ timestamp, và thuộc tính parentT — chỉ có trên các câu trả lời con — đặt tên cho mã định danh cha. Excel tái tạo cây hội thoại bằng cách khớp parentT với t; không có sự lồng nhau trong XML, chỉ là một danh sách phẳng cộng với các tham chiếu

Phần person là mảnh ghép mà hầu hết các triển khai thủ công thường bỏ quên. Mỗi authorId trong một threaded comment phải được phân giải thành một mục <person> trong xl/person/person.xml, được liên kết vào các mối quan hệ của workbook, với ghi đè content-type riêng của nó — nếu không Excel sẽ không có tên hiển thị để chỉ ra. HotXLS tự động xử lý việc đăng ký: AddThreadedComment tìm kiếm tác giả trên danh sách Persons của workbook theo tên hiển thị và tạo một mục với một GUID mới khi không tìm thấy, nhờ đó các bình luận liên tiếp của cùng một tác giả sẽ chia sẻ cùng một danh tính. Các mối quan hệ của worksheet nhận mối quan hệ threadedComments, các mối quan hệ của workbook nhận mối quan hệ person, và manifest của content-types nhận cả hai ghi đè, mà không có bất kỳ thứ gì xuất hiện trong code của bạn

Xây dựng chuỗi trả lời với AddThreadedComment và AddThreadedReply

AddThreadedComment tạo ra root của cuộc hội thoại: ParentId của nó để trống, đây là dấu hiệu đánh dấu nó là đầu của chuỗi. AddThreadedReply nhận một tham số ParentRef — tham chiếu ô nơi root tọa lạc — tìm kiếm root tại neo đó, và đóng dấu ParentId của câu trả lời mới bằng Id của root. Cả hai đều trả về TXLSXThreadedComment mới, vì vậy bạn có thể thiết lập timestamp hoặc trạng thái resolved trên đối tượng bạn vừa tạo

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;

Có hai chi tiết trong danh sách đó đáng được chú ý. Thứ nhất, Root.Ref được truyền thay vì một chuỗi tự xây dựng: tham chiếu mà thư viện tạo ra được đảm bảo khớp với những gì AddThreadedReply sẽ tìm kiếm, giúp loại bỏ cả một nhóm lỗi lệch neo một hàng. Thứ hai, cả hai tác giả đều được truyền dưới dạng tên hiển thị thông thường — việc đăng ký person, danh tính GUID và liên kết authorId đều diễn ra tự động phía sau các lệnh gọi. Mở file đã lưu trong Excel 365 và bạn sẽ thấy neo ô, thứ tự trả lời và hai người tham gia riêng biệt đều hoạt động: nhấp vào Trả lời và Excel sẽ thêm vào cùng một chuỗi hội thoại mà code của bạn đã bắt đầu

Đọc lại cuộc hội thoại: hành vi round-trip

HotXLS hỗ trợ round-trip cho threaded comment kể từ phiên bản 2.122.0: việc mở lại một package đã lưu sẽ xây dựng lại danh sách hội thoại của từng sheet và danh sách người tham gia của workbook từ các phần, do đó các mã định danh, liên kết trả lời và tên hiển thị vẫn tồn tại qua một chu kỳ đọc→ghi đầy đủ. Trình phân tích cú pháp person.xml trước bất kỳ nội dung sheet nào, điều này cho phép mỗi authorId phân giải thành một tên hiển thị tại thời điểm tải; các mã định danh person đã phân tích được bảo toàn nguyên trạng thay vì được tạo lại, do đó một workbook di chuyển qua lại giữa code của bạn và Excel sẽ giữ các danh tính tác giả ổn định qua số lượng vòng quay round trip tùy ý

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;

Việc lưu trữ dạng danh sách phẳng cộng với các tham chiếu hiển thị ở đây một cách có chủ ý: ThreadedComments liệt kê các câu trả lời theo thứ tự phần, và một ParentId trống xác định mỗi root. Nếu bạn cần dạng cây — ví dụ để xuất một nhật ký đánh giá — trước tiên hãy nhóm theo Ref và sau đó liên kết ParentId với Id trong nhóm của từng ô. Hành vi bảo-tồn-những-gì-đã-phân-tích này là một trường hợp của chính sách rộng hơn trong thư viện; bài viết về round-trip không tổn hao của theme, extension list và calc chain phủ bóng cách nguyên tắc tương tự được áp dụng cho phần còn lại của package

Trạng thái resolved, timestamp, và danh tính tác giả

Ba thuộc tính mang ngữ nghĩa đánh giá, và cả ba đều có thể ghi trên các đối tượng được trả về. Done ánh xạ tới thuộc tính done — Excel hiển thị một chuỗi đã giải quyết được thu gọn với một liên kết Mở lại (Reopen). DateTime is the ISO-8601 UTC creation stamp (dT); HotXLS thay thế bằng một giữ chỗ cố định khi bạn để trống để đảm bảo phần luôn hợp lệ, nhưng một timestamp thực sự là thứ làm cho lịch sử chuỗi có thể đọc được, vì vậy hãy thiết lập nó. Về phía danh tính, mỗi TXLSXPerson hiển thị ProviderId — thường là None cho các mục được tạo cục bộ — và một UserId bạn có thể điền bằng UPN hoặc email khi ứng dụng của bạn biết nó

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;

Một giới hạn cần nêu rõ ràng: HotXLS ghi phần tử <mentions> trống

Các bản ghi @-mention — cơ chế làm nổi bật tên của một đồng nghiệp bên trong văn bản bình luận và kích hoạt một thông báo trong Microsoft 365 — không được mô hình hóa, do đó văn bản chứa ký tự @ được lưu trữ dưới dạng văn bản thuần túy. Đối với một workbook đánh giá được tạo ra, điều đó hiếm khi là một tổn thất, nhưng nếu quy trình làm việc của bạn phụ thuộc vào các thông báo nhắc tên, hãy lập kế hoạch để một người dùng thêm những thông báo đó bên trong Excel. Danh tính tác giả trong phần person ghép đôi tự nhiên với các trường nguồn gốc cấp workbook được đề cập trong siêu dữ liệu workbook và thuộc tính tài liệu, nơi chứa phần còn lại của dấu vết kiểm toán của một tệp được tạo

Các phiên bản Excel cũ hơn làm gì với threaded comment?

Excel 2016 và các phiên bản cũ hơn xuất hiện trước mô hình threaded comment, và chúng chỉ đơn giản bỏ qua các phần chúng không hiểu. Khi chính Excel 365 lưu một cuộc hội thoại dạng chuỗi, nó cũng ghi một bóng legacy comment (legacy comment shadow) — một ghi chú giữ chỗ đọc là "[Threaded comment]…" — chính xác để các phiên bản cũ hơn hiển thị thứ gì đó tại ô được neo. HotXLS chỉ phát ra các phần threaded, không có bóng legacy, do đó một file được ghi bởi code của bạn sẽ không hiển thị chỉ báo bình luận nào khi mở trong Excel 2016. Nếu đối tượng người dùng của bạn bao gồm các bản cài đặt pre-365 và chú thích bắt buộc phải hiển thị ở đó, API AddComment cũ vẫn là kênh tương thích; chỉ cần tránh xếp cả hai mô hình trên cùng một ô mà không kiểm thử mọi phiên bản Excel bạn phân phối tới, vì cách một trình đọc điều hòa một note và một thread trên một ô là quyết định của trình đọc, không phải của bạn

Giới hạn cứng khác là chính định dạng file. Threaded comment là một cấu trúc OOXML không có đối tác BIFF8 tương đương, do đó phía .xls của HotXLS không có API threaded — một workbook bắt buộc phải ở định dạng nhị phân cũ bị giới hạn trong các note cổ điển. Trong thực tế, bảng quyết định rất ngắn gọn: ghi .xlsx cho Excel 365 hoặc các trình đọc Excel web/mobile, hãy sử dụng AddThreadedComment để có cuộc hội thoại thực tế; hướng tới Excel 2016 hoặc .xls, hãy sử dụng AddComment và chấp nhận mô hình note; kiểm toán một file bạn nhận được, hãy kiểm tra cả ThreadedCommentsComments, vì một workbook đã sống trong cả hai thế giới có thể mang cả hai một cách hợp pháp. Toàn bộ bề mặt API cho cả hai thế hệ được tài liệu hóa trên trang sản phẩm HotXLS Delphi Excel Component