Τεχνικό Άρθρο

Selection records BIFF8 και κύλιση pane στο HotXLS

Το HotXLS αποθηκεύει επιλογές worksheet και θέσεις κύλισης ανά pane μέσα από ένα pane-aware API κοινό και για τα δύο, TXLSWorksheet και TXLSXWorksheet: SelectAreas, GetSelectedAreas, ScrollWindow και TryGetWindowScroll. Για κλασικά αρχεία .xls, το HotXLS γράφει Selection records του BIFF8 (0x001D) με το πολύ 1369 περιοχές το καθένα, μετατρέπει τα λογικά ονόματα pane στα byte pane που ορίζει η μορφή, και κρατάει κάθε άξονα κύλισης στο record Window2 ή Pane εκεί που τον περιμένει το Excel

Το πρόβλημα συνήθως εμφανίζεται σε ένα εργαλείο συμψηφισμού ή ελέγχου. Το εργαλείο ανοίγει μια εξαγωγή καθολικού, βρίσκει κάθε κελί που διαφωνεί με το σύστημα πηγής, και αποθηκεύει το workbook με αυτά τα κελιά ήδη επιλεγμένα κάτω από μια παγωμένη γραμμή headers, ώστε ο ελεγκτής να προσγειώνεται στις διαφορές αντί να τις ψάχνει με scrolling. Με σαράντα διαφορές δουλεύει μια χαρά. Το αρχείο του μήνα έχει 3.000, και ένα μοναδικό Selection record με 3.000 περιοχές δεν μπορεί να υπάρξει: το body του θα χρειαζόταν 18.009 bytes, πάνω από το διπλάσιο από όσα χωράει ένα record BIFF8. Η θέση κύλισης έχει παρόμοια παγίδα. Σε ένα sheet με frozen panes, το «πού κοίταγε ο χρήστης» είναι τέσσερα panes που μοιράζονται δύο θέσεις γραμμών και δύο θέσεις στηλών, όχι μία συντεταγμένη

Γιατί μια μεγάλη επιλογή χρειάζεται πάνω από ένα Selection record;

Μια μεγάλη επιλογή χρειάζεται αρκετά records επειδή το body ενός record BIFF8 έχει όριο 8224 bytes και κάθε επιλεγμένη περιοχή κοστίζει σταθερά έξι bytes. Το [MS-XLS] §2.4.248 περιγράφει το Selection record ως ένα σταθερό μέρος 9 bytes (το byte pane, τα rwAct και colAct για το ενεργό κελί, το irefAct για την ενεργή περιοχή, και το cref για το πλήθος περιοχών) ακολουθούμενο από cref δομές RefU, καθεμία με δύο γραμμές 16-bit και δύο στήλες 8-bit. Το μεγαλύτερο πλήθος που χωράει είναι (8224 − 9) / 6 στρογγυλοποιημένο προς τα κάτω, δηλαδή 1369, και δίνει body 8223 bytes, ένα byte κάτω από το όριο. Το TXLSWorksheet.StoreSelectionGroup χρησιμοποιεί αυτή τη σταθερά ως MaxAreasPerRecord και γράφει μια μεγαλύτερη ομάδα ως διαδοχικά Selection records για το ίδιο pane, από 1369 περιοχές τη φορά

Η λεπτομέρεια που δαγκώνει είναι το irefAct. Κάθε κομμάτι επαναλαμβάνει την ίδια ενεργή γραμμή, ενεργή στήλη και ενεργό δείκτη περιοχής, και το irefAct αριθμεί τη συγκεντρωτική ακολουθία όλων των κομματιών, όχι τις περιοχές μέσα στο record που το κουβαλάει. Μια επιλογή μια περιοχή πάνω από το όριο το κάνει συγκεκριμένο: 1370 περιοχές με την τελευταία ενεργή γίνονται δύο records, το πρώτο με cref 1369 και το δεύτερο με cref 1, και τα δύο κουβαλάνε irefAct 1369. Αυτή η τιμή είναι μεγαλύτερη από το πλήθος περιοχών του ίδιου του δεύτερου record. Ένας reader που ελέγχει το irefAct απέναντι στο cref κάθε record απορρίπτει ένα έγκυρο αρχείο, και ένας reader που αντικαθιστά την κατάστασή του σε κάθε record χάνει τις πρώτες 1369 περιοχές. Ο reader του HotXLS προσαρτά διαδοχικά records του ίδιου pane σε μία ομάδα, απαιτεί κάθε κομμάτι να συμφωνεί στο ενεργό κελί και τον δείκτη, και τρέχει τον έλεγχο ορίων μόνο στο record EOF του worksheet, αφού η πλήρης ακολουθία γίνει γνωστή. Η υπερφόρτωση SelectAreas με το pane πρώτο δεν έχει επομένως οροφή των 1369 περιοχών. Επικυρώνει κάθε αναφορά A1 και τον ενεργό δείκτη πριν πάρει το write lock του worksheet, και επιστρέφει False με την προηγούμενη επιλογή ανέγγιχτη αν κάτι είναι κακοσχηματισμένο

Γιατί το HotXLS γράφει μια μεγάλη επιλογή worksheet ως αρκετά Selection records του BIFF8: το όριο body 8.224 bytes χωράει 9 σταθερά bytes συν 1369 περιοχές RefU των έξι bytes, ώστε 3.000 περιοχές να γίνονται τρία records του ίδιου pane με 1369, 1369 και 262, και το irefAct αριθμεί τη συγκεντρωτική ακολουθία ώστε 1370 περιοχές με την τελευταία ενεργή να δίνουν και στα δύο records irefAct 1369
Κάθε κομμάτι επαναλαμβάνει το ίδιο ενεργό κελί και δείκτη, ο reader του HotXLS προσαρτά διαδοχικά records του ίδιου pane σε μία ομάδα, και ο έλεγχος ορίων τρέχει μόνο στο record EOF αφού η πλήρης ακολουθία γίνει γνωστή
var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Diffs: TXLSSelectedAreas;
  I: Integer;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add;
    Sheet.FreezePanes(1, 1);           // η γραμμή headers και η στήλη A μένουν στη θέση τους

    SetLength(Diffs, 3000);
    for I := 0 to High(Diffs) do
      Diffs[I] := Format('C%d', [I + 2]);

    // Το πάγωμα μηδενίζει την αποθηκευμένη επιλογή, οπότε κάντε select μετά το πάγωμα.
    // 3000 περιοχές αποθηκεύονται ως τρία Selection records: 1369 + 1369 + 262
    if not Sheet.SelectAreas(xlspBottomRight, Diffs, 0) then
      raise Exception.Create('Selection rejected');

    Book.SaveAs('reconciliation.xls');
  finally
    Book.Free;
  end;
end;

Ποιο byte pane χρησιμοποιεί ένα Selection record;

Ένα Selection record ταυτοποιεί το pane του με τον αριθμητικό κωδικό που ορίζει η μορφή: 0 για bottom-right, 1 για top-right, 2 για bottom-left, και 3 για top-left. Η public απαρίθμηση TXLSPanePosition δηλώνεται με σειρά ανάγνωσης, xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight, οπότε το Ord(xlspTopLeft) είναι 0, που είναι το pane bottom-right μέσα στο αρχείο. Ένα cast της απαρίθμησης κατευθείαν στο byte pane θα έγραφε κάθε επιλογή top-left πάνω στο pane bottom-right χωρίς κανένα σφάλμα. Κάθε pane-aware σημείο εισόδου του HotXLS μετατρέπει την απαρίθμηση μέσω μιας ρητής εντολής case, ώστε όσοι καλούν να μην ασχολούνται καθόλου με τους αριθμητικούς κωδικούς. Ελέγχεται και η ύπαρξη pane: το pane top-right υπάρχει μόνο με κατακόρυφο split, το bottom-left μόνο με οριζόντιο, και το bottom-right μόνο με και τα δύο. Για pane που η τρέχουσα γεωμετρία split ή freeze δεν έχει, το SelectAreas επιστρέφει False, και το GetSelectedAreas επιστρέφει κενό array με ActiveAreaIndex -1, χωρίς να δημιουργήσει pane, αντικείμενο επιλογής ή κελί στο workbook

Πώς το HotXLS αντιστοιχίζει το TXLSPanePosition στο byte pane του Selection του BIFF8: η απαρίθμηση δηλώνεται με σειρά ανάγνωσης ώστε το Ord(xlspTopLeft) να είναι 0, ενώ το αρχείο ορίζει 0 για bottom-right, 1 για top-right, 2 για bottom-left και 3 για top-left, ώστε κάθε pane-aware σημείο εισόδου να μετατρέπει μέσω ρητής εντολής case
Ένα cast της απαρίθμησης κατευθείαν στο byte pane θα έγραφε κάθε επιλογή top-left πάνω στο pane bottom-right, οπότε το HotXLS ελέγχει και την ύπαρξη pane απέναντι στην τρέχουσα γεωμετρία split ή freeze πριν γράψει

Πού ζει η θέση κύλισης κάθε pane;

Η θέση κύλισης κάθε pane μοιράζεται σε δύο records, επειδή τέσσερα panes μοιράζονται μόνο δύο θέσεις γραμμών και δύο θέσεις στηλών. Σε ένα κλασικό workbook, η πρώτη ορατή γραμμή των πάνω panes και η πρώτη ορατή στήλη των αριστερών είναι τα Window2.rwTop και Window2.colLeft, ενώ η γραμμή των κάτω panes και η στήλη των δεξιών είναι τα Pane.rwTop και Pane.colLeft. Το ScrollWindow(xlspTopRight, R, C) γράφει επομένως Window2.rwTop και Pane.colLeft, και το να ορίσεις τη στήλη του pane top-right μετακινεί και το pane bottom-right, ακριβώς όπως τα δύο μοιράζονται μια οριζόντια scrollbar στο Excel. Οι public μέθοδοι χρησιμοποιούν αριθμούς γραμμών και στηλών με βάση το 1. Ένα ανύπαρκτο pane επιστρέφει False και μηδενίζει και τις δύο εξόδους του query, και μια συντεταγμένη εκτός ορίων απορρίπτεται πριν αλλάξει οποιοσδήποτε άξονας. Τίποτα εδώ δεν εξαρτάται από το πώς ζωγραφίζει το grid μια εφαρμογή προβολής. Ένα rendering control κρατάει δικά του TopRow και LeftCol, όπως περιγράφει το άρθρο για το rendering workbook σε custom VCL grid, και αυτά είναι κατάσταση runtime, όχι αυτό που αποθηκεύεται

Πού ζει κάθε άξονας κύλισης pane στο HotXLS: τέσσερα panes μοιράζονται δύο θέσεις γραμμών και δύο στηλών, ώστε η πάνω γραμμή και η αριστερή στήλη είναι Window2.rwTop και Window2.colLeft ενώ η κάτω γραμμή και η δεξιά στήλη είναι Pane.rwTop και Pane.colLeft, και το ScrollWindow(xlspTopRight, 1, 6) γράφει ένα πεδίο Window2 συν ένα Pane ώστε το bottom-right να ακολουθήσει
Το XLSX απλώνει τα ίδια δεδομένα πάνω στα attributes topLeftCell του sheetView και του pane, και το να συρρικνώσεις τα δύο επίπεδα σε ένα είναι ακριβώς ο τρόπος που μια θέση κύλισης πάνω ή αριστερά εξαφανίζεται σιωπηλά στο load

Το XLSX απλώνει τα ίδια δεδομένα πάνω σε δύο στοιχεία: το sheetView/@topLeftCell (ECMA-376 Part 1, §18.3.1.87) για το παράθυρο ως σύνολο και το παιδί pane/@topLeftCell (§18.3.1.66) για την κάτω δεξιά πλευρά ενός split. Και τα δύο attributes μπορούν να υπάρχουν μαζί. Το HotXLS διαβάζει πρώτα το εξωτερικό attribute στα πεδία επιπέδου παραθύρου, αφήνει το παιδί pane να παρακάμψει μόνο τα πεδία επιπέδου pane, και τα γράφει και τα δύο πίσω ξεχωριστά. Το να συρρικνώσεις τα δύο επίπεδα σε ένα είναι ακριβώς ο τρόπος που μια θέση κύλισης πάνω ή αριστερά εξαφανίζεται σιωπηλά στο load. Τα αντίγραφα worksheet κουβαλάνε και τα δύο επίπεδα και στις δύο μηχανές. Τα παλαιότερα σημεία εισόδου κρατούν την αρχική τους συμπεριφορά: τις κλασικές ιδιότητες ScrollRow και ScrollColumn, και τα zero-based XLSX SetPaneScroll και GetPaneScroll. Η ίδια η γεωμετρία freeze και split ρυθμίζεται με τις ρυθμίσεις επιπέδου sheet που καλύπτονται στο προστασία sheet, page setup και εκτύπωση

var
  Row, Col: Integer;
begin
  Sheet.FreezePanes(1, 1);

  // Bottom-right: κάτω άξονας γραμμών (Pane.rwTop) και δεξιός άξονας στηλών (Pane.colLeft)
  Sheet.ScrollWindow(xlspBottomRight, 500, 3);

  // Το top-right μοιράζεται τον δεξιό άξονα στηλών, οπότε αυτό μετακινεί και το bottom-right στη στήλη 6
  Sheet.ScrollWindow(xlspTopRight, 1, 6);

  if Sheet.TryGetWindowScroll(xlspBottomRight, Row, Col) then
    Memo1.Lines.Add(Format('Bottom-right starts at row %d, column %d', [Row, Col]));
    // Το bottom-right ξεκινά στη γραμμή 500, στήλη 6
end;

Τι γίνεται όταν ένα Selection record είναι κατεστραμμένο;

Όταν ένα Selection record είναι κατεστραμμένο, το HotXLS το κρατάει ως αδιαφανή bytes, αναφέρει διαγνωστικό κωδικό 1304 (xlsDiagnosticSelectionRecordInvalid), και ξαναγράφει το αρχικό body byte προς byte στο save. Πριν ένα record μπει στην ομάδα του pane του, ο reader το ελέγχει με σειρά. Το byte pane πρέπει να είναι 3 ή λιγότερο. Τα records ενός pane πρέπει να είναι συνεχόμενα στη ροή. Τα 9 σταθερά bytes πρέπει να υπάρχουν. Το cref πρέπει να είναι μεταξύ 1 και 1369, και το body ακριβώς 9 + cref × 6 bytes. Κάθε κομμάτι σε μια ομάδα πρέπει να συμφωνεί στο ενεργό κελί και το irefAct, το irefAct δεν πρέπει να έχει set το sign bit, η ενεργή στήλη πρέπει να βρίσκεται πάνω στο πλέγμα, και καμία περιοχή δεν επιτρέπεται να έχει αντιστραμένα όρια. Τα προβλήματα ενός μεμονωμένου φυσικού record αναφέρονται μία φορά ανά record. Οι αντιφάσεις που εμφανίζονται μόνο μετά τη συγκέντρωση, όπως irefAct που δείχνει πέρα από το συνολικό πλήθος περιοχών ή ενεργό κελί έξω από την αριθμημένη περιοχή, αναφέρονται μία φορά ανά ομάδα στο EOF. Μια άκυρη ομάδα μένει αόρατη στο typed API: το GetSelectedAreas επιστρέφει κενό array με δείκτη -1 για εκείνο το pane, ενώ κάθε άλλο pane συνεχίζει να δουλεύει

var
  I: Integer;
  D: TXLSDiagnostic;
begin
  if Book.Open('supplier-upload.xls') <> 1 then
    Exit;
  for I := 0 to Book.Diagnostics.Count - 1 do
  begin
    D := Book.Diagnostics[I];
    if D.Code = xlsDiagnosticSelectionRecordInvalid then
      Log.Add(Format('%s: record $%.4x kept opaque (%s)',
        [D.SheetName, D.RecordId, D.Message]));
  end;
end;

Πώς επιβιώνουν οι επιλογές από εισαγωγές γραμμών και στηλών;

Οι επιλογές επιβιώνουν από τις δομικές επεξεργασίες επειδή η εισαγωγή ή διαγραφή ολόκληρων γραμμών ή στηλών ξαναχαρτογραφεί κάθε εκπροσωπούμενη ομάδα pane και στις δύο μηχανές, classic και XLSX, μέσω ενός κοινού remapper. Οι περιοχές που επιβιώνουν κρατούν τη σειρά τους και η ενεργή περιοχή κρατάει την ταυτότητά της. Αν η ενεργή περιοχή διαγραφεί, ενεργή γίνεται ο πρώτος επιζών διάδοχος, και αν δεν υπάρχει, ο τελευταίος επιζών προκάτοχος. Αν διαγραφούν όλες οι περιοχές, η ομάδα καταρρέει σε ένα κελί στο όριο της διαγραφής, και ένα ενεργό κελί που δεν πέφτει πια μέσα στην επιλεγμένη περιοχή μετακινείται στην πάνω αριστερή γωνία εκείνης της περιοχής, ώστε ο δείκτης και η συντεταγμένη να μην αντιφάσκουν ποτέ. Τα όρια είναι σκόπιμα. Οι άκυρες κλασικές ομάδες παραλείπονται από τον remapper αντί να ξαναγραφτούν σε μια επινόηση, ώστε τα αρχικά τους bytes να συνεχίζουν να κάνουν round-trip. Η επεξεργασία ενός pane αντικαθιστά μόνο τα records εκείνου του pane και αφήνει τα άλλα byte-πανομοιότυπα. Το ODS δεν παίρνει καθόλου κατάσταση επιλογής pane, επειδή το ODF δεν έχει αντίστοιχη δομή προβολής worksheet για να την κουβαλήσει

Αν η εφαρμογή σας γράφει αρχεία .xls που οι χρήστες ανοίγουν και χρειάζεται να περιηγηθούν — για να δουν τα επισημασμένα κελιά, να συνεχίσουν από εκεί που σταμάτησαν, ή να μοιραστούν ένα παγωμένο dashboard — το pane-aware API επιλογής και κύλισης είναι μέρος του HotXLS Delphi spreadsheet component, και δουλεύει το ίδιο για XLS και XLSX