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
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
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;
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