Article technique

Largeur de colonne Excel et Max Digit Width en Delphi

Le PDF exporté place chaque limite de colonne un demi-caractère à gauche de l’endroit où Excel la dessine, et chaque cellule renvoyée à la ligne casse désormais à un autre endroit. La largeur de colonne Excel ne se mesure ni en caractères ni en points. Elle se mesure en unités de largeur max de chiffre (MDW) de la police Normal du classeur, et HotXLS mesure cette police avec GDI avant chaque construction de pagination. Le mode de défaillance est silencieux : rien ne lève, les largeurs enregistrées font l’aller-retour octet pour octet, et la géométrie reste décalée de quelques pour cent par colonne jusqu’à ce que la dérive accumulée pousse un tableau d’une page sur deux

Dans quelle unité se mesure la largeur de colonne Excel ?

Une largeur de colonne dans une feuille de calcul est un décompte de chiffres de la police Normal du classeur, pas une mesure absolue. ECMA-376 §18.3.1.13 définit l’attribut width de <col> en fonction de la largeur max de chiffre (Maximum Digit Width) de cette police à 96 dpi, et donne la conversion d’une largeur enregistrée vers les pixels comme une expression tronquante sur le MDW. Pour Calibri 11, la police qu’Excel livre comme style Normal, le MDW mesure 7 pixels. Passez la largeur par défaut de 8.43 unités dans la formule de la spécification avec un MDW de 7 et vous obtenez exactement 64 pixels, soit 48 points à 96 dpi. Ce sont les nombres qu’Excel lui-même rapporte, si bien qu’ils font un contrôle utile : si votre conversion reproduit 8.43 unités en 64 pixels, l’arithmétique est juste et seule l’entrée MDW peut encore être fausse

const
  // Largeur max de chiffre (MDW) de la police de corps par défaut en pixels à 96 dpi.
  // Calibri 11 mesure 7 px, ce qui reproduit les largeurs en pixels exactes
  // qu'Excel enregistre (8.43 unités -> 64 px -> 48 pt).
  DefaultMDW = 7;
  MinimumColumnWidth = 24.0;

function ColumnWidthToPointsMdW(Value: Double; MdW: Integer): Double;
var
  Pixels: Integer;
begin
  if Value <= 0 then
    Value := 8.43;
  if MdW <= 0 then
    MdW := DefaultMDW;
  Pixels := Trunc(((256 * Value + Trunc(128 / MdW)) / 256) * MdW) + 5;
  Result := Pixels * 0.75; // pixels 96 dpi -> points
  if Result < MinimumColumnWidth then
    Result := MinimumColumnWidth;
end;

HotXLS garde cette arithmétique dans exactement une fonction, dans l’unité lxPagination, si bien qu’il y a un seul endroit où la règle peut se tromper. Le + 5 est le remplissage qu’Excel ajoute pour les filets et les marges de cellule, le * 0.75 convertit les pixels 96 dpi en points PostScript, et le plancher à MinimumColumnWidth existe pour qu’une colonne pathologiquement étroite laisse encore une bande où le moteur de rendu peut dessiner une bordure. Le point d’entrée public ColumnWidthToPoints garde sa vieille signature à un argument et transmet un MDW mesuré à cette fonction, ce qui a permis au changement de comportement d’atterrir sans toucher un seul site d’appel

La chaîne de conversion de largeur de colonne HotXLS en Delphi, injectant la largeur max de chiffre mesurée de la police Normal du classeur dans la formule de la spécification pour qu’une largeur enregistrée de 8.43 unités devienne 64 pixels puis 48 points
La largeur enregistrée est un décompte de chiffres, si bien que le MDW mesuré de la police Normal est une entrée de la formule plutôt qu’un détail de style, et l’aller-retour 8.43 vers 64 vers 48 vérifie l’arithmétique

Pourquoi une police Normal autre que Calibri déplace chaque limite

La dérive est multiplicative, et c’est pourquoi elle se lit comme un bogue de rendu plutôt que comme un bogue d’unités. Le MDW est un facteur sur la largeur, pas un décalage. Poussez le MDW de 7 à 8 et la colonne par défaut de 8.43 unités passe de 64 pixels à 72, un saut de 8 pixels ou 6 points sur une colonne. Dix colonnes de cela et le bord droit du tableau a bougé de presque un pouce. Les classeurs qui déclenchent cela sont tout à fait ordinaires : tout ce qu’un outil de reporting génère en tamponnant Arial ou Segoe UI dans le style Normal, tout ce qui sort d’un modèle d’export ERP, tout ce qu’un client a restylé une fois puis oublié

Deux systèmes de mise en page apparentés héritent de l’erreur au lieu de la causer. Les régions fusionnées font la somme des largeurs en points de leurs colonnes membres, si bien qu’une fusion qui tenait sur une page dans Excel peut déborder après une dérive de MDW, ce qui vaut la peine de se rappeler quand vous construisez des modèles de rapport à cellules fusionnées. Le mode Réduire pour ajuster compare la largeur de texte mesurée à cette même largeur de colonne, si bien qu’un MDW faux change aussi quelles cellules rétrécissent et de combien. La même famille de confusion d’unités apparaît dans les ancres de dessin, où la géométrie d’image et la mise à l’échelle EMU a sa propre chaîne de conversion où se tromper

Deux règles de colonnes HotXLS comparées, l’une mesurée avec un MDW de 7 pixels et l’autre avec 8, montrant comment le saut par colonne de 64 à 72 pixels s’accumule sur dix colonnes tandis que les régions fusionnées et le mode Réduire pour ajuster héritent de l’erreur
Parce que le MDW multiplie plutôt qu’il ne décale, une seule mesure fausse déplace chaque limite de colonne, et les régions fusionnées et le mode Réduire pour ajuster héritent de la dérive sans que rien ne lève

Comment HotXLS mesure le MDW à l’exécution

HotXLS résout le MDW depuis le classeur lui-même au lieu de supposer une constante, et deux procédures font le travail. PaginationApplyNormalFont lit la police du style Normal hors du classeur et tourne au sommet de la construction de pagination, avant que toute géométrie de colonne ne soit calculée ; elle se réinitialise d’abord sur Calibri 11, si bien qu’un classeur sans table de polices ne peut pas hériter d’un état périmé d’une construction précédente. La police du style Normal est fonts[0] dans styles.xml, exposée par le composant comme Workbook.Fonts[0]

// Lit fonts[0] (la police du style Normal) hors du classeur de la feuille de calcul.
// Les feuilles classiques sans table de polices gardent la valeur par défaut Calibri 11.
procedure PaginationApplyNormalFont(Worksheet: TObject);
var
  Sh: TXLSXWorksheet;
  Fnt: TXLSXFont;
begin
  PaginationNormalFontName := 'Calibri';
  PaginationNormalFontSize := 11;
  if not (Worksheet is TXLSXWorksheet) then
    Exit;
  Sh := TXLSXWorksheet(Worksheet);
  if (Sh.Workbook = nil) or (Sh.Workbook.Fonts.Count < 1) then
    Exit;
  Fnt := Sh.Workbook.Fonts[0];
  if Fnt.Name <> '' then
    PaginationNormalFontName := Fnt.Name;
  if Fnt.Size > 0 then
    PaginationNormalFontSize := Fnt.Size;
end;

La seconde procédure, PaginationMeasureMdW, demande à GDI l’étendue du caractère unique '0' par le biais de GetTextExtentPoint32W sur un canevas bitmap hors écran partagé, retombe sur tmAveCharWidth de GetTextMetricsW quand l’appel d’étendue échoue, et retombe sur DefaultMDW quand ni l’un ni l’autre n’est disponible. Son cache est un emplacement unique indexé par (name, size), ce qui sonne grossier jusqu’à ce que vous regardiez le motif d’accès : une construction de pagination demande la même police Normal sur chaque colonne de chaque page, si bien qu’un seul emplacement a un taux de réussite quasi parfait et coûte trois comparaisons par appel

Que se passe-t-il sans table de polices, sans GUI, ou avec une police manquante ?

HotXLS se dégrade vers la constante Calibri 11 dans chaque cas où la vraie police Normal ne peut pas être déterminée, et il le fait silencieusement par conception. Les feuilles BIFF classiques sont le cas courant : les formats patrimoniaux ne portent aucun pool de polices XLSX auquel fonts[0] pourrait se référer, si bien que le garde de type sort tôt et le MDW par défaut de 7 tient. Ce n’est pas une correction, c’est le comportement précédent préservé délibérément, pour que l’ajout de la mesure au chemin XLSX ne puisse pas faire régresser la sortie au format classique

La dépendance GDI est l’avertissement honnête. La mesure tourne contre un contexte de périphérique Windows, si bien que le chemin suppose un hôte Windows avec la police installée. Dans un service ou un agent de construction sans tête, les métriques de texte GDI se résolvent généralement encore, mais une police qui n’est pas installée sur cette machine est remplacée par le mappeur de polices et vous mesurez le remplaçant à la place. Il n’échoue jamais bruyamment ; il renvoie un nombre plausible pour la mauvaise police. Si les exports côté serveur doivent correspondre à une référence de bureau, installez sur l’hôte d’export les polices que vos modèles nomment, ou figez la police Normal avant d’invoquer le chemin d’export PDF de feuille de calcul

var
  Book: TXLSXWorkbook;
  Exporter: TXLSPDFExport;
begin
  Book := TXLSXWorkbook.Create;
  Exporter := TXLSPDFExport.Create;
  try
    Book.Open('quarterly-report.xlsx');

    // Figez la police Normal pour que le MDW mesuré sur cet hôte soit celui
    // contre lequel la mise en page a été conçue, pas un remplaçant du mappeur de polices.
    if Book.Fonts.Count > 0 then
    begin
      Book.Fonts[0].Name := 'Calibri';
      Book.Fonts[0].Size := 11;
    end;

    Exporter.UseWorksheetPageSetup := True;
    Exporter.SaveAsPDF(Book, 'quarterly-report.pdf');
  finally
    Exporter.Free;
    Book.Free;
  end;
end;

Caches de mesure, et celui qui a planté sur Win64

Une fois que la mesure de texte est un aller-retour GDI plutôt qu’une multiplication, elle doit être mise en cache, et c’est dans la mise en cache à l’intérieur d’une passe de rendu que ce travail a saigné. La boucle de Réduire pour ajuster descend la taille de police par pas de 0.5 pt et re-mesure après chaque pas, si bien qu’une cellule peut appeler PaginationMeasureTextWidth une douzaine de fois avec la même chaîne, et le retour à la ligne des mots l’appelle encore par ligne candidate. Un mémo indexé par nom de police, taille et texte réduit cela à un appel GDI par chaîne distincte, stocké dans une TStringList comme paires nom/valeur

L’autre cache ajouté à côté n’était pas aussi propre. La passe de rendu 5 résout le pool de polices par cellule via FontIndex, et son mémo utilisait des tableaux dynamiques parallèles avec un FontMemoCount maintenu à la main. La première version oubliait d’appeler ResetFontMemo au début de chaque page, si bien que le compteur continuait de grimper à travers les pages tandis que les tableaux non, et le code écrivait au-delà de la fin de tous. Sur Win32 cela gribouillait tranquillement dans le tas adjacent et terminait ; sur Win64 cela levait une violation d’accès sur une écriture à 0x538 immédiatement. La leçon généralisable : un cache adossé à des tableaux détenu dans une variable de niveau unité doit être réinitialisé à l’entrée de chaque passe qui l’utilise, parce qu’une liste de chaînes ou un dictionnaire pardonne une réinitialisation manquante en grandissant et des tableaux parallèles non

Comment HotXLS résout la police Normal du classeur, mesure sa largeur max de chiffre par le biais de GDI avec deux solutions de repli, et met le résultat en cache, à côté des deux mémos de passe de rendu et de la règle de réinitialisation dont un cache à tableaux parallèles a besoin
Le MDW est résolu depuis le classeur et mesuré avec GDI une fois par police, puis mis en cache par clé, tandis que les mémos de passe de rendu montrent pourquoi un cache à tableaux parallèles doit être réinitialisé à l’entrée de chaque passe

Vérifier votre propre conversion

Vous n’avez pas besoin du composant pour vérifier tout cela. Prenez un classeur dont la police Normal n’est pas Calibri 11, lisez une largeur depuis <col width="..."/>, et passez-la deux fois dans la formule de la spécification, une fois avec un MDW de 7 et une fois avec le MDW que votre moteur de rendu mesure réellement pour cette police ; si les réponses diffèrent et que votre sortie correspond à la première, vous avez trouvé la dérive. La géométrie de colonne est une de ces parties d’un moteur de tableur qui est soit invisible soit la seule chose que tout le monde remarque, et la faire correctement signifie traiter la police Normal comme une entrée de la mise en page plutôt que comme un détail de style. Si vous construisez des applications Delphi ou C++Builder qui lisent, écrivent, rendent et impriment des classeurs Excel sans Office installé, le composant tableur HotXLS pour Delphi gère la mesure du MDW, le modèle de pagination et le pipeline PDF derrière un seul ensemble de classes VCL