Technical Article

Nhân bản worksheet XLSX trong Delphi với HotXLS

Bạn đã làm đúng một worksheet. Dải tiêu đề được gộp ô, độ rộng cột khớp với dữ liệu, hai hàng đầu đã được cố định, vùng in và lề đã sẵn sàng cho bản xuất A4 gọn gàng, và tab được tô màu để bộ phận tài chính dễ tìm. Bây giờ báo cáo cần mười hai bản như vậy, mỗi bản cho một khu vực, tất cả đều bắt đầu từ cùng một bố cục. Nếu phải dựng lại worksheet đó bằng mã mười hai lần, sai lệch nhỏ sẽ tích tụ: khu vực 7 lệch cột đi một chút, khu vực 11 mất chế độ cố định, và chẳng ai để ý cho đến khi PDF xuất hiện trên bàn quản lý. Điều bạn thực sự cần là phiên bản lập trình của lệnh Excel right-click, Move or Copy, Create a copy: lấy worksheet đã hoàn thiện và tạo ra các bản sao độc lập.

Công cụ XLSX trong HotXLS, một thư viện Delphi và C++Builder gốc đọc và ghi file Excel mà không điều khiển Excel, vốn đã có thể di chuyển sheet, xóa sheet, và sao chép vùng ô giữa các sheet. Nhưng cho đến v2.91.0, nó chưa thể nhân bản toàn bộ worksheet chỉ bằng một lời gọi. Bản phát hành đó bổ sung hai điểm vào: TXLSXWorksheet.CopyFrom, sao chép trạng thái cấp sheet từ một worksheet sang worksheet khác, và TXLSXSheets.Duplicate, thêm một sheet mới và chạy CopyFrom giúp bạn. Điểm thú vị không nằm ở chuyện nó sao chép được gì. Điều đáng chú ý là ranh giới có chủ ý giữa những phần được sao chép sâu và những phần không được, và vì sao ranh giới đó nằm đúng chỗ ấy.

Một lời gọi để nhân bản một worksheet đã hoàn thiện

Thao tác cấp cao là Duplicate. Hãy đưa vào chỉ số nguồn tính từ 1, và nó sẽ trả về một worksheet hoàn toàn mới, phản chiếu bố cục và dữ liệu của bản gốc. Quy ước chỉ số khớp với Items[] ở phía XLSX, nên sheet đầu tiên có chỉ số 1, không phải 0; nếu truyền chỉ số ngoài phạm vi, bạn nhận nil thay vì ngoại lệ, đúng theo hợp đồng lỗi mà phần còn lại của bộ sưu tập sheet XLSX đang dùng.

var
  Book: TXLSXWorkbook;
  Template, Copy: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Template := Book.Sheets.Add('Template');
    Template.Cells[1, 1].Value := 'Quarterly Statement';
    Template.Range['A1:C1'].Merge;
    Template.ColWidth[1] := 18;
    Template.FreezePanes(2, 1);          // freeze top row + first column
    Template.TabColorIsAuto := False;
    Template.TabColor := $FF1F4E79;

    // Clone with an explicit name...
    Copy := Book.Sheets.Duplicate(1, 'Region-North');
    // ...or let it pick the Excel-style default name.
    Copy := Book.Sheets.Duplicate(1);    // -> "Template (2)"

    Book.SaveAs('regions.xlsx');
  finally
    Book.Free;
  end;
end;

Có hai điểm trong đoạn mã đó đáng dừng lại một chút. Thứ nhất, FreezePanes nhận đối số theo thứ tự hàng trước, FreezePanes(ARow, ACol), nên nó khớp với cách đánh chỉ mục Cells[Row, Col]; bản sao thừa hưởng nguyên vẹn đường chia vùng cố định. Thứ hai, phương thức được đặt tên là Duplicate chứ không phải cái tên hiển nhiên hơn là Copy, và đó không phải sở thích về phong cách. Copy là một routine chuẩn trong unit System, được dùng liên tục cho chuỗi và mảng động. Nếu một phương thức của lớp cũng tên Copy, nó sẽ che khuất routine đó ngay trong thân phương thức và tạo ra đúng thứ rối về phân giải tên khiến bạn vấp phải sáu tháng sau. Duplicate tránh hoàn toàn vấn đề này và đọc đúng ngay tại nơi gọi.

Tên mặc định tuân theo quy tắc của chính Excel

Khi bạn gọi overload một tham số, hoặc truyền vào một chuỗi tên rỗng, sheet mới sẽ được đặt tên theo sheet nguồn kèm hậu tố (2), và hậu tố đó tăng dần cho đến khi tên là duy nhất. Nhân bản sheet Template một lần thì bạn có Template (2); nhân bản lại lần nữa thì bạn có Template (3), vì Template (2) đã được dùng. Điều này phản chiếu cách Excel tự tạo tên từ lệnh Create a copy của nó, nên workbook do mã của bạn sinh ra trông đúng như người dùng kỳ vọng từ một bản sao thủ công. Kiểm tra tính duy nhất chạy trên tập sheet đang tồn tại, nên nó cũng sẽ bỏ qua những tên bạn tự đặt thủ công, không chỉ những tên xuất hiện từ các lần nhân bản trước.

Nếu bạn đang sinh một sheet cho mỗi khu vực hoặc mỗi tháng, hãy dùng overload có tên rõ ràng. Một quy ước như Region-North, Region-South dễ tham chiếu về sau hơn nhiều so với một chuỗi hậu tố (2), (3), và nó giữ cho các tên đã định nghĩa cùng công thức liên sheet dễ đọc.

CopyFrom sao chép sâu những gì

Bên trong, Duplicate thêm sheet rồi gọi CopyFrom(ASource), và bạn cũng có thể gọi trực tiếp khi muốn sao chép sang một sheet đã tạo sẵn. CopyFrom chặn trước hai trường hợp rỗng: sao chép từ nil, hoặc sao chép một sheet lên chính nó, cả hai đều trả về ngay và không làm gì. Phần còn lại chính là thao tác sao chép, và nó được mở rộng có chủ ý.

Dữ liệu ô được xử lý trước tiên. CopyFrom hỏi nguồn về UsedRange, khung bao gọn của các ô có dữ liệu và các vùng gộp, rồi dùng lại cơ chế CopyRangeTo hiện có để mang mọi giá trị, công thức, và chỉ mục kiểu định dạng theo từng ô sang đích bắt đầu từ A1. Trên lớp ô, nó còn phát lại toàn bộ trạng thái cấp sheet làm cho một template trông hoàn chỉnh:

  • Các vùng gộp, được tái tạo theo tọa độ để dải tiêu đề phủ đúng cùng một hình chữ nhật.
  • Độ rộng cột và chiều cao hàng, cùng các danh sách ẩn, thu gọn, và mức phân cấp, được sao chép nguyên trạng để các hàng và cột không mặc định khớp chính xác.
  • Chế độ cố định khung và trạng thái hiển thị: mức zoom, hiển thị đường lưới và giá trị bằng 0, hướng phải sang trái, và kiểu xem.
  • Trạng thái bảo vệ cùng các tùy chọn quyền theo từng hành động, để template đã khóa vẫn khóa theo cùng một cách.
  • Cả khối thiết lập trang: lề, hướng giấy, khổ giấy, tỷ lệ và vừa trang, vùng in, tiêu đề in, header và footer, cùng các cờ in đường lưới và in tiêu đề.
  • Vùng AutoFilter, màu tab, và trạng thái hiển thị của sheet.

Kết quả là một sheet in, lọc, và trình bày giống hệt nguồn. Và vì các ô, vùng gộp, và danh sách kích thước được tái tạo vật lý trên sheet mới thay vì trỏ chung, bản sao hoàn toàn độc lập. Ghi 999 vào một ô trên bản sao, và nguồn vẫn giữ nguyên giá trị cũ; tính độc lập đó là thuộc tính quan trọng nhất của một bản sao dùng cho các báo cáo khu vực chạy song song, và demo SheetCopy đi kèm có kiểm tra điều này một cách rõ ràng.

Phần nào giữ nông, và vì sao

Bây giờ đến phần nói thẳng. Biểu đồ, ảnh nhúng, bảng XLSX, kiểm tra hợp lệ dữ liệu, và các quy tắc định dạng có điều kiện đều không được sao chép. Đây là một ranh giới có chủ ý và đã được ghi nhận, không phải thiếu sót, và đáng để hiểu lý do để bạn chuẩn bị trước thay vì ngạc nhiên.

Mỗi nhóm đó mang theo danh tính và các tham chiếu không thể sống sót qua một phép sao chép trường ngây thơ. Một biểu đồ trỏ tới vùng dữ liệu nguồn và sở hữu một quan hệ vẽ trong gói OOXML; sao chép đối tượng mà không ánh xạ lại quan hệ và tham chiếu chuỗi sẽ tạo ra biểu đồ hiển thị trên dữ liệu sai, hoặc một gói mà Excel báo cần sửa. Một bảng có tên phải duy nhất trong workbook, một hàng tiêu đề gắn với các cột cụ thể, và quan hệ tự sinh riêng. Định dạng có điều kiện và xác thực dữ liệu gắn với các vùng tọa độ và, trong trường hợp xác thực, có thể tham chiếu vùng khác bằng công thức. Sao chép sâu những thứ này một cách đúng đắn nghĩa là phải viết lại tham chiếu và tạo danh tính mới, đó là công việc thực sự với các điểm lỗi thực sự. Làm dở dang, bằng cách sao chép đối tượng nhưng không sao chép tham chiếu, còn tệ hơn không sao chép gì cả: nó tạo ra một file mở lên có lời nhắc sửa lỗi và âm thầm bỏ mất nội dung. Vì vậy engine chỉ sao chép những gì nó có thể sao chép sạch sẽ, còn các tập hợp mang tham chiếu thì để lại cho bên gọi, người hiểu đích cần trỏ tới đâu.

Trong thực tế, quy trình cho một template giàu nội dung là: nhân bản sheet để lấy ô, bố cục, và thiết lập in; rồi dựng lại biểu đồ, bảng, validation, hoặc định dạng có điều kiện trên bản sao bằng cùng API bạn đã dùng để tạo chúng lần đầu. Vì bạn đang tạo lại chúng theo chính các vùng của bản sao, các tham chiếu sẽ đúng ngay từ đầu. Với một biểu đồ đọc A1:C10, hãy thêm biểu đồ mới trên bản sao và trỏ vào A1:C10 của bản sao; với AutoFilter mà bạn muốn hoạt động, lưu ý rằng vùng filter được chuyển qua, nên bạn chỉ cần áp lại tiêu chí cột. Các quy tắc định dạng có điều kiện và xác thực dữ liệu mà bạn sẽ thêm lại bằng cùng các lời gọi đã mô tả trong bài viết về ô gộp và bố cục template báo cáo, nơi đi qua bảng gộp và mô hình vùng mà bản sao kế thừa.

Nhân bản worksheet phù hợp ở đâu trong một pipeline báo cáo

Nhân bản worksheet là bạn đồng hành tự nhiên của việc tạo nội dung dựa trên placeholder. Cách tiếp cận neo token trong hướng dẫn tạo báo cáo từ template trong Delphi giải quyết bài toán đổ dữ liệu vào một bố cục do người khác chỉnh sửa; còn nhân bản giải quyết bài toán cần dùng bố cục đó nhiều lần trong cùng một workbook. Ghép hai thứ lại, mô hình rất gọn: giữ một sheet Template nguyên vẹn với token, vùng gộp, và thiết lập in, rồi với mỗi khu vực hoặc kỳ báo cáo hãy gọi Duplicate, điền dữ liệu của lát cắt đó vào bản sao, rồi tiếp tục. Template nguyên gốc không bao giờ bị sửa, nên nó luôn là nguồn tin cậy cho bản sao tiếp theo, và mỗi sheet đầu ra đều bắt đầu từ cùng một bố cục byte-for-byte giống hệt.

Một lưu ý về thứ tự thao tác sẽ tránh được một lớp nhầm lẫn. Hãy nhân bản sheet trước khi đổ dữ liệu vào nó, không phải sau. Template nên giữ cấu trúc và định dạng, chứ không phải số liệu của quý trước, và việc nhân bản một sheet rỗng nhưng đã có kiểu dáng nghĩa là mỗi bản sao đều bắt đầu sạch sẽ. Nếu bạn nhân bản một sheet đã có dữ liệu, dữ liệu đó sẽ đi theo, vì CopyFrom sao chép vùng đã dùng một cách trung thực; đôi khi đó là điều bạn muốn, nhưng với báo cáo phân nhánh thì thường không phải.

Một thói quen kiểm tra nhanh

Vì sự tách biệt giữa sao chép sâu và sao chép nông là vô hình cho đến khi bạn cố kiểm tra, hãy thêm một bước kiểm tra năm dòng vào job thay vì chỉ tin rằng mọi thứ đã đi qua. Sau khi nhân bản, hãy đọc lại các tín hiệu cấu trúc mà bản sao phải thừa hưởng và xác nhận chúng khớp với nguồn.

Copy := Book.Sheets.Duplicate(1, 'Region-North');
WriteLn(Format('merged=%d  colA=%.1f  freezeRow=%d  tabAuto=%d',
  [Copy.MergedCells.Count, Copy.ColWidth[1],
   Copy.FreezeRow, Integer(Copy.TabColorIsAuto)]));
// Prove independence: mutate the copy, confirm the source is untouched.
Copy.Cells[2, 2].Value := 999;
// Template.Cells[2, 2].Value is still whatever it was.

Số lượng vùng gộp, độ rộng một cột, hàng cố định, và cờ màu tab cho bạn biết lớp dữ liệu thực sự được sao chép đã đi qua. Riêng với bất kỳ sheet nào có biểu đồ, bảng, validations, hoặc định dạng có điều kiện, hãy xem đó là danh sách cần dựng lại trên bản sao: việc chúng không có mặt là đúng thiết kế, và cách xử lý chỉ là vài lời gọi, không phải một bug report. Mô hình tinh thần đó, sâu ở chỗ an toàn và nông ở chỗ các tham chiếu có thể vỡ, chính là toàn bộ câu chuyện về cách dùng tính năng này tốt.

Nhân bản worksheet và phép sao chép trạng thái sheet bằng CopyFrom được mô tả ở đây đi kèm trong v2.91.0 của thành phần bảng tính Delphi gốc HotXLS Delphi spreadsheet component, cùng với mẫu SheetCopy có thể chạy được để kiểm tra toàn bộ chu trình nhân bản rồi thay đổi đến tận cùng.