Bài viết kỹ thuật

Mở và lưu bảng tính ODS trong Delphi bằng HotXLS

Một backend báo cáo Delphi đã xuất .xlsx nhiều năm liền đón nhận một yêu cầu mới: quy tắc mua sắm của một khách hàng khu vực công bắt buộc phải xuất ra OpenDocument Spreadsheet, và các nhà phân tích ở tài khoản đó gửi lại các chỉnh sửa của họ dưới dạng file .ods đã lưu từ LibreOffice. Vậy nên giờ cùng một đoạn mã phải ghi ODS lẫn đọc nó. HotXLS, thư viện bảng tính Object Pascal thuần gốc của losLab cho Delphi và C++Builder, xử lý cả hai chiều mà không cần cài Excel hay LibreOffice ở bất cứ đâu. Điều nó không làm là khiến hai chiều đó đối xứng nhau. Xuất mang theo nhiều hơn hẳn những gì nhập khôi phục lại được, và một nhóm giả định điều ngược lại sẽ chứng kiến công thức và định dạng bốc hơi đâu đó giữa lần chỉnh sửa của khách hàng và báo cáo tiếp theo, không có lỗi nào để chỉ ra nguyên nhân

Hỗ trợ ODS nằm trên lớp giao diện XLSX, không phải XLS

HotXLS đi kèm hai hệ phân cấp class độc lập trong một gói duy nhất: TXLSWorkbook trong unit lxHandle cho file .xls BIFF8 nhị phân, và TXLSXWorkbook trong unit lxHandleX cho các gói .xlsx OOXML. Mọi điểm vào OpenDocument - OpenODS, SaveAsODS, GetODSSheetNames - đều treo trên TXLSXWorkbook. Vị trí đặt này không phải ngẫu nhiên. Một gói ODS, như được đặc tả trong OASIS ODF 1.3, là một archive zip mang theo một thành viên mimetype, một manifest, và một thân content.xml, điều này khiến nó trở thành người anh em họ về mặt cấu trúc với zip OOXML; BIFF8 là một luồng bản ghi nhị phân từ những năm 1990 không có điểm chung nào với chúng

Vị trí đặt đó mang một lợi thế thực tế: một workbook .xls kiểu cũ không thể trở thành .ods chỉ trong một lệnh gọi. Bạn phải bắc cầu nội dung BIFF vào mô hình XLSX trước, bằng SaveXLSWorkbookAsXLSX từ unit lxXlsxExport, mở lại kết quả đó thông qua TXLSXWorkbook, rồi mới xuất từ đó. Cầu nối này không lossless, và đáng để biết các khoảng hở trước khi bạn xây dựng dựa trên nó. Nó sao chép giá trị, công thức, định dạng số, font, fill, và độ rộng cột. Nó bỏ qua viền, range đã merge, comment, biểu đồ, và conditional formatting. Một nguồn .xls với định dạng nặng sẽ tới ODS trông đơn giản hơn lúc nó rời đi, và đó là một đặc tính của cầu nối, không phải của trình ghi ODS

Việc phát hiện ở phía nhập là tự động. Phương thức Open thông thường nhận diện một gói ODS bằng thành viên mimetype của nó, quay về kiểm tra content.xml ở cấp cao nhất khi thành viên đó vắng mặt, nên một đường mã "mở bất cứ thứ gì người dùng đã tải lên" chung chung không cần tự dò đuôi file. Sau khi mở, thuộc tính SourceFormat báo cáo nhánh nào đã kích hoạt

Sơ đồ bố cục class của HotXLS trong Delphi, nơi mọi điểm vào ODS nằm trên TXLSXWorkbook và một cầu SaveXLSWorkbookAsXLSX chuyển nội dung .xls BIFF8 sang
Mọi entry point OpenDocument đều treo trên TXLSXWorkbook, và một .xls kiểu cũ chỉ với tới ODS qua cầu BIFF sang XLSX có hao hụt

Xuất ra ODS với TODSExportOptions

Bản thân lệnh gọi xuất chỉ là một dòng; đối tượng options xung quanh nó mang theo những quyết định một người review sẽ hỏi về sau:

var
  Book: TXLSXWorkbook;
  Opts: TODSExportOptions;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-report.xlsx');
    Opts := TODSExportOptions.Create;        // caller sở hữu và giải phóng đối tượng này
    try
      Opts.Generator := 'ReportService 4.2'; // ghi đè meta:generator
      Opts.IncludeCharts := True;
      Opts.IncludeImages := True;
      Book.SaveAsODS('quarterly-report.ods', Opts);
    finally
      Opts.Free;
    end;
  finally
    Book.Free;
  end;
end;

Đối tượng options này thuộc sở hữu của bên gọi. HotXLS sẽ không giải phóng nó, đó là lý do tại sao try..finally bên trong tồn tại chứ không phải tùy chọn. Hai thuộc tính thực sự thay đổi kết quả xuất ra, chứ không chỉ dán nhãn cho nó, đáng được xem xét kỹ hơn. Đặt IncludeCharts := False làm nhiều hơn là chỉ ẩn biểu đồ: nó tước bỏ các sub-document biểu đồ và các mục manifest của chúng ra khỏi gói, đây chính xác là điều bạn muốn khi bên tiêu thụ là một pipeline dữ liệu sẽ vấp phải chúng. Generator ghi đè chuỗi meta:generator của ODF, mà nếu không sẽ đọc là HotXLS/<version>; hãy ghi đè nó khi công cụ downstream lấy dấu vân tay của bên tạo file để định tuyến hỗ trợ. Nếu không điều gì trong số đó áp dụng, hãy bỏ qua hoàn toàn đối tượng options. Gọi SaveAs(FileName, xlsxOpenDocumentSpreadsheet) tương đương với SaveAsODS dùng giá trị mặc định, và các overload nhận stream trên cả hai cho phép bạn ghi gói thẳng vào một response HTTP mà không cần file tạm

Đường nhập đọc những gì - và những gì nó cố tình bỏ qua

Hãy đọc kỹ phần này trước khi bạn hứa hẹn với bất kỳ ai về độ trung thực round-trip. Việc nhập ODS trong HotXLS có chủ đích là một đường đi nhẹ nhàng. Nó bảo tồn các giá trị ô vô hướng và kết quả cache mà mỗi công thức mang theo vào lúc lưu, và nó mở rộng các hàng và cột lặp lại ra thành lưới đầy đủ. Nó không mang theo style, biểu thức công thức ODS, hay drawing

Lựa chọn về công thức là thứ nhiều khả năng cắn nhất, và nó được đưa ra có chủ đích. Một ô ODF lưu hai thứ song song: biểu thức công thức, được viết bằng phương ngữ OpenFormula định nghĩa trong ODF 1.3 Part 4, và giá trị cuối cùng mà ứng dụng tạo ra đã tính cho nó. Dịch OpenFormula sang cú pháp công thức Excel là một bài toán chuyển đổi phương ngữ riêng của nó, với những trường hợp biên thực sự xoay quanh vốn từ vựng hàm, cú pháp tham chiếu, và mô hình lỗi. Đọc giá trị cache thay vào đó né tránh toàn bộ nhóm lỗi dịch sai âm thầm đó, nên những con số bạn nhập vào chính xác là những con số người gửi đã thấy lần cuối. Cái giá phải trả là chúng đến dưới dạng những con số, không phải những công thức sống đã tạo ra chúng

Chế độ thất bại cần thiết kế xoay quanh theo ngay sau đó: một bảng tính có tổng đúng khi LibreOffice lưu nó lần cuối sẽ nhập vào với những con số đúng, nhưng những con số đó giờ là hằng số. Chỉnh sửa một ô đầu vào, tính lại, và không gì di chuyển cả - công thức đã biến mất, chỉ còn lại kết quả cuối cùng của nó. Nếu workflow cần công thức sống sau khi nhập, hãy tái lập chúng bằng lập trình từ các quy tắc nghiệp vụ của riêng bạn thông qua Cell.Formula, mà trên lớp giao diện XLSX nhận biểu thức không có dấu bằng ở đầu

Thiết kế xoay quanh vòng round-trip bất đối xứng

Xuất render từ mô hình workbook đầy đủ trong bộ nhớ: giá trị, style, và, nếu bạn yêu cầu, biểu đồ và hình ảnh. Nhập chỉ trả về giá trị. Vì vậy chặng .xlsx sang .ods có độ trung thực cao, còn chặng .ods sang .xlsx mang lại giá trị và kết quả cache nhưng không có style và không có công thức sống. Nối hai chặng lại với nhau và sự bất đối xứng đó cộng dồn. Một chu trình đầy đủ .xlsx sang .ods rồi lại về .xlsx ghi mọi thứ trung thực trên chặng đi ra và mất style cùng công thức trên chặng quay về, ngay cả khi không có gì sai ở bất kỳ bước nào

Sơ đồ vòng đi-về ODS không đối xứng của HotXLS từ Delphi: xuất đầy đủ độ trung thực từ mô hình workbook trong bộ nhớ và nhập chỉ-giá-trị để công thức thành các hằng số
Export render toàn bộ mô hình trong bộ nhớ trong khi import trả về giá trị và kết quả đã cache, nên một vòng .xlsx sang .ods rồi về .xlsx đầy đủ lặng lẽ đánh rơi style và công thức còn sống
Book := TXLSXWorkbook.Create;
try
  Book.Open('vendor-revision.ods');          // định dạng tự động dò
  if Book.SourceFormat = xlsxOpenDocumentSpreadsheet then
  begin
    // Giá trị và kết quả công thức đã cache có mặt sau khi nhập
    // ODS; style và công thức sống thì không. Hãy dựng lại bất cứ gì
    // pipeline downstream phụ thuộc vào trước khi lưu.
    Book.Sheets[0].Cells[2, 5].Formula := 'SUM(B2:D2)';
    Book.SaveAs('vendor-revision.xlsx');
  end;
finally
  Book.Free;
end;

Khuôn mẫu kiến trúc rút ra từ điều này: hãy coi các file .ods đến như những nguồn cấp dữ liệu, không phải những tài liệu để chỉnh sửa tại chỗ. Giữ workbook chuẩn ở dạng .xlsx, đọc giá trị ra từ các bản chỉnh sửa của khách hàng, và phát sinh ODS mới theo yêu cầu từ bản chuẩn đó. Việc xác minh thuộc về cả hai phe - mở các file đã xuất trong LibreOffice Calc, bên tiêu thụ ODF tham chiếu, và trong Excel, vốn đã đọc ODS nhiều năm nhưng bất đồng với LibreOffice ở những rìa hỗ trợ biểu đồ và style. Số lượng sheet, một số ô chính, và sự hiện diện của biểu đồ tạo thành một smoke check đủ dùng cho mỗi hồ sơ xuất

Phân loại một file ODS trước khi cam kết nhập nó

Khi một endpoint chấp nhận tải lên, việc liệt kê tên sheet rẻ hơn hẳn một lượt parse đầy đủ và bắt được những bất ngờ cấu trúc từ sớm:

Sơ đồ cổng phân loại tệp tải lên của HotXLS trong Delphi, nơi GetODSSheetNames từ chối các package ODS không đọc được và các sheet bị thiếu trước khi một lượt nhập đầy đủ chạy
Một probe GetODSSheetNames rẻ hơn hẳn một lần parse đầy đủ và bắt được lỗi sheet bị đổi tên khi thông báo lỗi còn có thể nêu tên tệp
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
  if Book.GetODSSheetNames('incoming.ods', Names) <= 0 then
    raise Exception.Create('not a readable ODS package');
  if Names.IndexOf('Data') < 0 then
    raise Exception.Create('revision is missing the Data sheet');
finally
  Book.Free;
  Names.Free;
end;

Quy ước giá trị trả về khiến người ta vấp phải: các lệnh gọi HotXLS nói chung trả về một số đếm dương hoặc 1 khi thành công và -1 khi thất bại, xóa sạch danh sách khi thất bại, nên hãy kiểm tra <= 0 thay vì so sánh với một giá trị dương cụ thể duy nhất. GetODSSheetNames không reset cũng không điền dữ liệu vào instance workbook, nên một đối tượng thăm dò duy nhất có thể kiểm tra cả một thư mục file đến. Những kiểm tra cấu trúc như thế này bắt được thất bại thực tế phổ biến nhất - một nhà phân tích đổi tên hoặc xóa một sheet trước khi gửi lại bản chỉnh sửa - ngay tại cổng vào, nơi thông báo lỗi vẫn có thể nêu tên file và sheet bị thiếu thay vì lộ ra dưới dạng một tham chiếu nil ba tầng sâu hơn

Nếu bạn đang xây dựng một pipeline chuyển đổi rộng hơn xung quanh điều này, khuôn mẫu workbook audit và conversion workbench cho thấy cách kiểm kê các tính năng của một file trước khi chọn định dạng đích, và hướng dẫn hiệu năng workbook lớn giữ các lượt xuất hàng loạt trong giới hạn bộ nhớ hợp lý

HotXLS là một thư viện bảng tính Delphi và C++Builder thuần gốc kèm đầy đủ mã nguồn; danh sách tính năng đầy đủ và chi tiết cấp phép nằm trên trang sản phẩm HotXLS Delphi Component