Article technique

Selection BIFF8 et défilement des volets dans HotXLS

Un outil de réconciliation trouve 3 000 cellules en désaccord avec le grand livre et veut que le .xls sauvegardé s'ouvre avec toutes sélectionnées sous une ligne d'en-tête figée. Sur le papier, c'est un seul enregistrement Selection. En octets, ça fait 9 + 3 000 × 6 = 18 009, et un corps d'enregistrement BIFF8 s'arrête à 8 224. Le même classeur soulève une seconde question qui sonne triviale et ne l'est pas : sur une feuille figée en B2, "la position de défilement", ce sont quatre volets partageant deux décalages de lignes et deux décalages de colonnes, et ces quatre nombres vivent dans deux enregistrements différents

Les deux problèmes sont de l'arithmétique de format : la sélection est coupée en enregistrements qu'un lecteur doit recoudre, le volet est nommé par un code qui ne suit pas l'ordre de lecture, et chaque axe de défilement atterrit dans l'enregistrement depuis lequel Excel le lit

Pourquoi 3 000 zones sélectionnées exigent trois enregistrements Selection

[MS-XLS] §2.4.248 dispose l'enregistrement Selection (0x001D) en une partie fixe de 9 octets suivie des zones. La partie fixe détient l'octet de volet, rwAct et colAct pour la cellule active, irefAct pour l'index de la zone active, et cref pour le compte de zones. Chaque zone est une structure RefU avec deux lignes 16 bits et deux colonnes 8 bits. Le plus grand compte qui tient est (8 224 − 9) / 6 arrondi vers le bas, ce qui donne 1 369 zones et un corps de 8 223 octets, un octet sous le plafond

HotXLS prend 1 369 comme plafond par enregistrement. Quand TXLSWorksheet sauvegarde un groupe de sélection avec plus de zones que ça, il écrit des enregistrements Selection consécutifs pour le même volet, 1 369 zones à la fois, et chaque enregistrement répète le même octet de volet, la même cellule active et le même index actif. La sélection de réconciliation de 3 000 cellules sort en trois enregistrements portant 1 369, 1 369 et 262 zones. Les appelants ne voient jamais ce découpage ; ils remettent un tableau de références A1

var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Diffs: TXLSSelectedAreas;
  I: Integer;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add;
    Sheet.FreezePanes(1, 1);           // la ligne d'en-tête et la colonne A restent en place

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

    // FreezePanes efface la sélection stockée, donc sélectionnez après le gel.
    // 3000 zones sont sauvegardées en trois enregistrements Selection : 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;

L'ordre des appels compte ici. FreezePanes reconstruit la disposition des volets et efface toute sélection par volet que la feuille portait, donc une sélection antérieure serait partie au moment de la sauvegarde. Sur une feuille non gelée, le volet inférieur droit n'existe pas encore, et le même appel SelectAreas renverrait simplement False. Gelez, puis sélectionnez

Sur quoi pointe irefAct quand une sélection s'étale sur plusieurs enregistrements ?

Dans une sélection divisée, irefAct indexe toute la séquence agrégée de zones à travers tous les morceaux, et chaque morceau porte la même valeur. Une sélection d'une zone au-delà de la limite rend ça concret : 1 370 zones avec la dernière active deviennent deux enregistrements, le premier avec cref 1 369 et le second avec cref 1, et tous deux portent irefAct 1 369. Dans le second enregistrement, cet index est plus grand que le compte de zones de l'enregistrement lui-même. Ça a l'air cassé si vous lisez un enregistrement à la fois, et deux sortes de lecteurs y trébuchent

  • Un lecteur qui confronte irefAct à cref à l'intérieur de chaque enregistrement rejette un fichier parfaitement valide au second morceau
  • Un lecteur qui remplace son état de sélection à chaque enregistrement Selection ne garde que le dernier morceau et perd les 1 369 premières zones

Le lecteur de HotXLS ajoute les enregistrements consécutifs d'un même volet dans un seul groupe et exige de chaque morceau l'accord sur la ligne active, la colonne active et irefAct. Le contrôle de plage sur irefAct, et le contrôle que la cellule active se trouve vraiment dans la zone qu'elle pointe, ne tournent qu'à l'arrivée de l'enregistrement EOF de la feuille, quand la séquence complète est connue. Vérifier plus tôt serait du code plus simple et la mauvaise réponse pour toute sélection au-delà de 1 369 zones

Pourquoi HotXLS écrit une grande sélection de feuille en plusieurs enregistrements Selection BIFF8 : le plafond de corps de 8 224 octets tient 9 octets fixes plus 1369 zones RefU de six octets, si bien que 3 000 zones deviennent trois enregistrements du même volet de 1369, 1369 et 262, et irefAct indexe la séquence agrégée, donc 1370 zones dont la dernière est active donnent aux deux enregistrements irefAct 1369
Chaque fragment répète la même cellule active et le même index, le lecteur HotXLS concatène les enregistrements consécutifs d'un même volet en un seul groupe, et le contrôle de plage ne s'exécute qu'à l'enregistrement EOF une fois la séquence complète connue

Côté écriture, la surcharge de SelectAreas volet-d'abord n'a pas de plafond de 1 369 zones propre. Elle analyse chaque référence avant de toucher à la feuille : les marqueurs $ sont acceptés et retirés, une plage inversée comme C9:A2 est normalisée en A2:C9, et une entrée vide ou imparsable fait échouer tout l'appel. L'index actif doit tomber dans le tableau et rester au plus 32 767, car irefAct est un champ 16 bits signé. Tout échec renvoie False et laisse la sélection précédente exactement telle quelle

Les codes de volet ne suivent pas l'ordre de lecture

Un enregistrement Selection nomme son volet avec le code numérique que le format définit : 0 pour inférieur droit, 1 pour supérieur droit, 2 pour inférieur gauche et 3 pour supérieur gauche. L'énumération publique TXLSPanePosition est déclarée en ordre de lecture, xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight. Donc Ord(xlspTopLeft) vaut 0, et 0 dans le fichier signifie inférieur droit. Un écrivain qui caste l'énumération directement dans l'octet de volet place chaque sélection supérieure gauche sur le volet inférieur droit, et le fichier reste structurellement valide parce que les deux sont des codes de volet légaux

Chaque point d'entrée conscient des volets dans HotXLS convertit l'énumération par une instruction case explicite, donc le code applicatif ne manipule jamais les codes numériques. L'existence des volets est aussi vérifiée contre la géométrie de gel ou de division courante. Le volet supérieur gauche existe toujours. Supérieur droit exige une division verticale, inférieur gauche une division horizontale, et inférieur droit les deux. Pour un volet que la feuille n'a pas, SelectAreas renvoie False et GetSelectedAreas renvoie un tableau vide avec ActiveAreaIndex à -1, et aucun des deux appels ne crée de volet, de sélection ni de cellule en chemin

Comment HotXLS mappe TXLSPanePosition sur l'octet de volet Selection BIFF8 : l'énumération est déclarée en ordre de lecture donc Ord(xlspTopLeft) vaut 0, tandis que le fichier définit 0 pour bas-droite, 1 pour haut-droite, 2 pour bas-gauche et 3 pour haut-gauche, si bien que chaque point d'entrée conscient des volets convertit via une instruction case explicite
Caster l'énumération directement dans l'octet de volet écrirait chaque sélection haut-gauche sur le volet bas-droite, donc HotXLS vérifie aussi l'existence du volet contre la géométrie courante de division ou de gel avant d'écrire

La feuille garde aussi l'ancienne surcharge à deux arguments SelectAreas(Areas, ActiveAreaIndex), qui écrit dans le volet actuellement actif et renvoie un statut Integer. Sur une feuille à volets figés, c'est souvent un pari sur un état que vous n'avez pas posé vous-même. Préférez la surcharge qui prend un TXLSPanePosition d'abord pour tout ce qui tourne sur des feuilles gelées ou divisées ; le volet fait alors partie de l'appel, et un résultat False vous dit que la géométrie et la demande ne sont pas d'accord. Relire le résultat suit la même forme :

var
  Areas: TXLSSelectedAreas;
  Active: Integer;
begin
  Areas := Sheet.GetSelectedAreas(xlspBottomRight, Active);
  if Active < 0 then
    Memo1.Lines.Add('No usable selection on the bottom-right pane')
  else
    Memo1.Lines.Add(Format('%d areas, active area %s',
      [Length(Areas), Areas[Active]]));
end;

Où réside la position de défilement de chaque volet ?

La position de défilement de chaque volet se partage entre deux enregistrements, car quatre volets n'ont que deux positions de lignes et deux positions de colonnes. Dans un classeur classique, la première ligne visible des volets du haut et la première colonne visible des volets de gauche de lignes et deux positions de colonnes. Dans un classeur classique, la première ligne visible des volets du haut et la première colonne visible des volets de gauche sont Window2.rwTop et Window2.colLeft. La ligne des volets du bas et la colonne des volets de droite sont Pane.rwTop et Pane.colLeft. Le volet supérieur droit prend donc sa ligne dans Window2 et sa colonne dans Pane, et le volet inférieur gauche fait l'inverse

ScrollWindow et TryGetWindowScroll suivent cette disposition exactement et utilisent des numéros de lignes et de colonnes à base 1. ScrollWindow(xlspTopRight, R, C) écrit Window2.rwTop et Pane.colLeft, ce qui veut dire que poser la colonne du volet supérieur droit déplace aussi le volet inférieur droit. Ça colle à Excel, où les deux volets de droite partagent une seule barre de défilement horizontale. Un volet absent fait renvoyer False à ScrollWindow et fait poser les deux sorties de TryGetWindowScroll à zéro. Une ligne ou colonne hors plage est rejetée avant que le moindre axe ne change, donc une colonne mauvaise ne peut pas laisser une ligne à moitié appliquée derrière elle

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

  // Inférieur droit : axe de lignes du bas (Pane.rwTop) et axe de colonnes de droite (Pane.colLeft)
  Sheet.ScrollWindow(xlspBottomRight, 500, 3);

  // Supérieur droit partage l'axe de colonnes de droite, donc ceci déplace aussi inférieur droit vers la colonne 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]));
    // Inférieur droit démarre à la ligne 500, colonne 6
end;
Où réside chaque axe de défilement de volet dans HotXLS : quatre volets partagent deux positions de rangée et deux de colonne, si bien que la rangée supérieure et la colonne de gauche sont Window2.rwTop et Window2.colLeft tandis que la rangée inférieure et la colonne de droite sont Pane.rwTop et Pane.colLeft, et ScrollWindow(xlspTopRight, 1, 6) écrit un champ Window2 plus un champ Pane pour que le bas-droite suive
XLSX étale les mêmes données sur les attributs topLeftCell de sheetView et de pane, et aplatir les deux couches en une seule est exactement ainsi qu'une position de défilement supérieure ou de gauche disparaît silencieusement au chargement

XLSX stocke les mêmes deux couches dans deux éléments : sheetView/@topLeftCell (ECMA-376 Partie 1, §18.3.1.87) pour la fenêtre dans son ensemble et l'enfant pane/@topLeftCell (§18.3.1.66) pour le côté inférieur droit d'une division. Les deux attributs peuvent être présents à la fois. Un chargeur qui n'en garde qu'un perd soit les lignes du haut soit les colonnes de gauche de chaque feuille figée qu'il touche. TXLSXWorksheet lit l'attribut externe dans les champs au niveau fenêtre d'abord, laisse l'enfant pane ne surcharger que les champs au niveau volet, et réécrit les deux séparément. ScrollWindow et TryGetWindowScroll ont les mêmes signatures et le même sens sur TXLSXWorksheet, et copier une feuille transporte les deux couches dans les deux formats

Les propriétés plus étroites sont toujours là pour le code qui n'a besoin que d'une paire d'axes : ScrollRow et ScrollColumn sur la feuille classique, et les SetPaneScroll et GetPaneScroll à base zéro pour le volet inférieur droit sur XLSX. Un contrôle de rendu garde ses propres TopRow et LeftCol, ainsi que le décrit l'article sur le rendu de classeurs dans une grille VCL personnalisée, et cet état d'exécution est séparé de ce qui se sauvegarde. La géométrie de gel et de division elle-même se pose avec les réglages au niveau feuille couverts dans protection de feuille, mise en page et impression

Les enregistrements Selection corrompus sont gardés octet pour octet

Quand un enregistrement Selection est malformé, HotXLS garde son corps en octets opaques, rapporte le code de diagnostic 1304 (xlsDiagnosticSelectionRecordInvalid) comme avertissement, et réécrit le corps d'origine inchangé à la sauvegarde. Des fichiers venant d'exporteurs inconnus ou d'écrivains BIFF artisanaux peuvent contenir de tels enregistrements. Avant qu'un enregistrement ne rejoigne le groupe de son volet, le lecteur vérifie un octet de volet de 3 au plus, des enregistrements contigus par volet, un cref entre 1 et 1 369 avec un corps d'exactement 9 + cref × 6 octets, des morceaux d'accord sur la cellule active et un irefAct sans son bit de signe, une colonne active sur la grille, et aucune zone inversée. Les problèmes à l'intérieur d'un seul enregistrement physique sont rapportés une fois par enregistrement ; les contradictions qui n'apparaissent qu'après agrégation, comme un irefAct pointant au-delà du compte total de zones ou une cellule active hors de la zone indexée, sont rapportées une fois par groupe à l'EOF

Un groupe invalide reste hors de l'API typée : GetSelectedAreas renvoie un tableau vide avec l'index -1 pour ce volet, et les autres volets continuent de marcher normalement

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 est votre propre TStrings ; HotXLS ne remplit que Diagnostics
      Log.Add(Format('%s: record $%.4x kept opaque (%s)',
        [D.SheetName, D.RecordId, D.Message]));
  end;
end;

Réparer l'enregistrement, en bornant irefAct ou en jetant la zone inversée, était l'autre option. Le préserver est la valeur par défaut plus sûre : une sélection réparée est une sélection que personne n'a choisie, et les octets d'origine seraient partis. Le message de diagnostic nomme le contrôle qui a échoué, comme "le compte de zones ne correspond pas à la longueur de l'enregistrement" ou "les enregistrements d'un même volet ne sont pas contigus", et RecordId vaut toujours $001D pour ce code

Les insertions de lignes et colonnes remappent la sélection de chaque volet

Insérer ou supprimer des lignes ou colonnes entières remappe la sélection de chaque volet par une routine partagée unique, dans le moteur classique comme dans le moteur XLSX. Les zones survivantes gardent leur ordre et se décalent avec les cellules qu'elles couvrent, et la zone active garde son identité. Si la zone active est supprimée, la première zone survivante après elle devient active ; si rien ne suit, c'est la dernière zone survivante avant elle. Si toutes les zones sont supprimées, le groupe se réduit à une seule cellule à la frontière de suppression. Une cellule active qui ne tombe plus dans la zone choisie se déplace vers le coin supérieur gauche de cette zone, pour que l'index et la coordonnée ne puissent jamais se contredire dans le fichier sauvegardé

Les groupes marqués invalides au chargement sont laissés tranquilles par le remappeur, donc leurs octets d'origine font toujours l'aller-retour après l'édition. Changer la sélection d'un volet ne réécrit que les enregistrements de ce volet et laisse les enregistrements des autres volets identiques octet pour octet. Les fichiers ODS ne portent aucun état de sélection par volet, car OpenDocument n'a pas de structure de vue de feuille par volet équivalente ; les sélections et positions de défilement de volets s'appliquent aux XLS et XLSX

Si votre application sauvegarde des classeurs que des gens ouvrent pour examiner des cellules marquées, reprendre là où ils s'étaient arrêtés, ou lire un tableau de bord figé, les appels de sélection et de défilement conscients des volets sont livrés avec le HotXLS Delphi spreadsheet component et se comportent pareillement pour une sortie .xls et .xlsx