Bài viết kỹ thuật

Nhúng chart object lên worksheet với HotXLS

HotXLS đặt được một chart ngay trên worksheet, neo vào một cell range, thay vì nhét nó lên một chart sheet riêng. Nói theo kiểu BIFF8, điều đó nghĩa là ghi một drawing shape với OBJ record type 5 và đậu chart substream ở cuối sheet record stream — đúng layout Excel tạo ra và đúng chỗ reader tìm kiếm

Sự phân biệt này quan trọng với bất kỳ ai generate báo cáo vận hành. Một chart sheet là mái nhà tử tế cho một visual tiêu đề đơn lẻ. Một bảng phân tích theo vùng theo tháng thì muốn chart nằm cạnh những con số nó tổng hợp, trên cùng sheet, cỡ đúng bằng khối cell nó thuộc về, để người đọc scroll một lần thay vì chuyển tab và đánh mất ngữ cảnh

Phần đọc đã có sẵn, phần ghi thì chưa

Sự bất đối xứng này đáng được gọi tên vì nó định hình công việc. HotXLS vốn đọc được chart nhúng: khi worksheet record stream chứa một BOF được đánh dấu là chart substream, parser chuyển ngữ cảnh, thu thập các chart record, và ở EOF đóng lại trả chúng về cho drawing shape mà OBJ record giới thiệu. Đường đó đã được mọi workbook do Excel tạo ra mà library từng mở rèn luyện qua

Cái thiếu là phía authoring, và hệ quả hữu ích là writer mới có một specification chuẩn xác để bám vào: tạo ra byte layout mà reader hiện có đã biết gắn lại. Không có acceptance criterion nào tốt hơn cho một tính năng binary format khi bạn có sẵn một reader viết độc lập mà bạn không được quyền đụng vào

Một chart nhúng được làm từ gì

Ba mảnh phải khớp nhau. Drawing layer đóng góp một host-control shape, object layer đóng góp một OBJ record mà common object data của nó khai object type 5, và record stream đóng góp chính chart substream. Các option flag trên OBJ record là những gì Excel ghi cho một chart frame: positioned, locked, automatic line và automatic fill — thứ làm chart nhúng hành xử như chart bản địa khi người dùng click vào

HotXLS neo một chart substream BIFF8 vào worksheet Delphi qua ba mảnh khớp nhau: host-control shape của drawing layer, OBJ record mà common object data khai object type 5, và chuỗi chart record đậu ở cuối sheet record stream, nơi một chart BOF chuyển ngữ cảnh parser và EOF đóng lại gắn các record về chỗ cũ
Ba layer cùng mang một chart nhúng: drawing shape neo nó, OBJ record gán nhãn nó là chart host, và chart substream ở cuối sheet stream cung cấp các record mà reader gắn lại

Anchor đáng một ghi chú vì nó là nguồn phổ biến của các bug lệch một đơn vị. API HotXLS nhận số row và column đánh từ một, khớp phần còn lại của library, còn client anchor ghi vào file thì đánh từ không. Phép chuyển nằm bên trong AddChartObject, nên caller ở lại trong hệ tọa độ họ dùng ở mọi nơi khác, nhưng ai đem một hex dump đối chiếu với lời gọi của chính mình cần nhớ mình đang đọc bên nào của ranh giới đó

var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Series: array[0..1] of TXLSChartSeriesInfo;
begin
  Book := TXLSWorkbook.Create(nil);
  try
    Book.LoadFromFile('regional-sales.xls');
    Sheet := Book.Sheets[0];

    FillChar(Series, SizeOf(Series), 0);
    Series[0].Name := 'Actual';
    Series[0].Categories := 'Data!$A$2:$A$13';
    Series[0].Values := 'Data!$B$2:$B$13';
    Series[0].DataLabels.ShowValue := True;
    Series[0].HasDataLabels := True;

    Series[1].Name := 'Target';
    Series[1].Categories := 'Data!$A$2:$A$13';
    Series[1].Values := 'Data!$C$2:$C$13';
    Series[1].SecondaryAxis := True;

    // Neo vào E2:M20 trên sheet này, đánh số từ một
    Sheet.AddChartObject(xlsChartTypeColumn, 'Regional sales',
      'Month', 'Amount', Series, 2, 5, 20, 13);

    Book.SaveToFile('regional-sales-charted.xls');
  finally
    Book.Free;
  end;
end;

FillChar trên series array không phải trang trí. TXLSChartSeriesInfo mang theo vài sub-records tùy chọn — data labels, style theo từng series, trendline và error bar — mỗi cái được chặn bởi một boolean, và một record trên stack được khởi tạo nửa vời sẽ đưa cho emitter những flag chẳng ai set. Zero mảng trước, rồi set những field bạn thật sự muốn

Series reference dạng nào được đường nhúng chấp nhận?

Range A1 thuần nằm trong cùng workbook, và giới hạn này là chủ ý chứ không phải sơ suất. Mọi reference được resolve theo sheet list của workbook và đổi thành external reference index mà các chart record cần. Named range hay reference sang workbook ngoài sẽ fallback thành placeholder với parsed expression độ dài 0, nên chart vẫn ghi ra sạch sẽ nhưng riêng series đó không có data source cho tới khi bạn trỏ nó về một range

Mức chấp nhận series reference của HotXLS trên đường chart BIFF8 nhúng: range kiểu A1 như Data!$B$2:$B$13 trong cùng workbook được resolve theo sheet list thành external reference index mà chart record cần, trong khi named range và reference workbook ngoài fallback thành placeholder với parsed expression độ dài 0, cả hai đều có trên AddChartSheet
Chỉ range kiểu A1 thuần trong cùng workbook compile thành series reference của chart; mọi thứ khác ghi ra sạch sẽ dưới dạng placeholder cho tới khi được trỏ lại, và đường đầy đủ nằm ở AddChartSheet

Lý do là một sự đánh đổi kỹ thuật thẳng thắn. Đường compile reference đầy đủ nằm trên route chart sheet, bọc trong worksheet-collection layer, và việc nhấc nó ra một cách sạch sẽ nghĩa là nhân bản một trăm dòng logic resolve cho một trường hợp hiếm gặp trong thực tế. Chart nhúng gần như luôn vẽ các cell trên chính sheet của nó hoặc một data sheet anh em. Named và external reference đã có trên đường chart sheet qua AddChartSheet, nên chẳng có gì bị mất, chỉ là phải vào từ một entry point khác

Mọi thứ còn lại trong series model hoạt động giống hệt trên cả hai route. Gắn secondary axis, style line, fill và marker theo series, trendline, error bar và data label đều là một phần của TXLSChartSeriesInfo và đều được emit cùng một cách, nên một chart definition di chuyển giữa object nhúng và chart sheet chỉ thay đổi đúng lời gọi. Cơ chế axis-group đứng sau flag secondary axis được nói trong secondary axis group khi ghi BIFF

Vì sao chart title đọc ra chỉ còn hai ký tự?

Vì một character count bị truyền vào chỗ mong đợi byte count, và BIFF Unicode string khiến lỗi này dễ viết và khó thấy. Một BIFF Unicode string ngắn mở đầu bằng character count và một flags byte, mà flags byte mang bit high-byte cho biết payload là một byte hay hai byte mỗi ký tự. Đọc một payload 16-bit với character count như thể đó là byte length thì bạn nhận đúng một nửa chuỗi: series tên Sales quay về thành Sa, và chart title cụt đi cùng một kiểu vì title và series label dùng chung đường decode

Cái khiến defect này đáng chú ý là nó tái diễn ba lần trong cùng một họ record: một lần ở trendline name, một lần ở pivot chart name, một lần ở chart title. Mỗi lần trông như một bug mới của một tính năng mới. Cả ba đều là cùng một phép nhân bị thiếu. Luật cuối cùng chấm dứt chuyện này là thuần cơ học và nên áp không cần phán xét: mỗi khi đọc một trong những string này, tra flag high-byte trước và nhân character count với payload width trước khi đụng vào buffer. Chi tiết cấp record nằm trong decode character count của XLUnicodeString và flag high-byte

// Chart nhúng dùng chung drawing layer với image và shape, nên
// drawing sẵn có trên sheet được giữ nguyên. AddChartObject
// trả về index của object vừa tạo
var
  ObjIndex: Integer;
begin
  ObjIndex := Sheet.AddChartObject(xlsChartTypeLine, 'Trend',
    'Week', 'Units', Series, 2, 8, 18, 16);
  if ObjIndex < 0 then
    raise Exception.Create('chart object was not created');
end;

Chart nhúng đứng đâu so với các phương án thay thế

Có ba route và chúng trả lời ba câu hỏi khác nhau. Một chart object nhúng thuộc về chỗ có data của nó trên worksheet và là cái đa số report cần. Một chart sheet hợp với một visual trình diễn đơn lẻ và cho bạn đường compile reference đầy đủ. Giữ nguyên một chart hiện có từ file đã load, không đụng chạm, là câu trả lời đúng khi workbook đến từ Excel với format mà chẳng ai muốn một library diễn giải lại; hành vi pass-through đó được mô tả trong ChartML được bảo toàn và combination chart

Vì chart nhúng đi trên drawing layer, nó tồn tại cùng image và shape trên cùng một sheet thay vì thay thế chúng, và mô hình chung của layer đó được nói trong chart, image và drawing trong HotXLS. Cả ba route đều có trong HotXLS Delphi spreadsheet component, nên lựa chọn là về chuyện report nên trông ra sao chứ không phải về chuyện library diễn tả được gì

Điểm phương pháp luận mới là thứ đáng giữ lại. Khi một tính năng binary format đã có sẵn reader, hãy dựng writer bám theo reader thay vì bám theo cách bạn hiểu specification. Reader tích lũy nhiều năm va chạm với những file mà application thật sự tạo ra, kể cả những phần specification nói lỏng, và một writer thỏa mãn reader đó xác suất thỏa mãn Excel cao hơn nhiều