Teknisk artikel

Excel-kolonnebredde og Max Digit Width (MDW) i Delphi

Den eksporterede PDF lægger hver kolonnegrænse et halvt tegn til venstre for, hvor Excel tegner den, og hver ombrydt celle bryder nu et andet sted. Excel-kolonnebredde måles ikke i tegn eller punkter. Den måles i Max Digit Width (MDW)-enheder af arbejdsbogens Normal-font, og HotXLS måler den font med GDI før hver pagineringsbuild. Fejlmåden er stille: intet kaster en fejl, de gemte bredder round-tripper byte for byte, og geometrien afviger stadig med et par procent pr. kolonne, indtil den akkumulerede drift skubber en tabel på én side ud på to

Hvilken enhed måles Excel-kolonnebredde i?

En kolonnebredde i et regneark er et antal ciffertegn i arbejdsbogens Normal-font, ikke en absolut måling. ECMA-376 §18.3.1.13 definerer width-attributten på <col> ud fra den fonts Maximum Digit Width ved 96 dpi og giver konverteringen fra en gemt bredde tilbage til pixels som et trunkerende udtryk over MDW. For Calibri 11, som Excel afskiber som Normal-typografien, måler MDW 7 pixels. Før standardbredden på 8.43 enheder gennem specifikationsformlen med MDW 7, og du får præcis 64 pixels, hvilket er 48 punkter ved 96 dpi. Det er tallene, Excel selv rapporterer, så de er en nyttig kontrol: hvis din konvertering gengiver 8.43 enheder til 64 pixels, er aritmetikken rigtig, og kun MDW-inputtet kan stadig være forkert

const
  // Maximum digit width (MDW) for standardbrødtekstfonten i pixels ved 96 dpi.
  // Calibri 11 måler 7 px, hvilket gengiver de præcise pixelbredder,
  // Excel gemmer (8.43 enheder -> 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; // 96 dpi-pixels -> punkter
  if Result < MinimumColumnWidth then
    Result := MinimumColumnWidth;
end;

HotXLS holder den aritmetik i præcis én funktion, i lxPagination-enheden, så der er ét enkelt sted, hvor linealen kan tage fejl. + 5 er den padding, Excel tilføjer til gitterlinjer og cellemarginer, * 0.75 konverterer 96 dpi-pixels til PostScript-punkter, og gulvet ved MinimumColumnWidth findes, så en patologisk smal kolonne stadig efterlader en stribe, rendereren kan tegne en kant ind i. Det offentlige indgangspunkt ColumnWidthToPoints beholder sin gamle signatur med ét argument og videresender en målt MDW til denne funktion, hvilket var det, der lod adfærdsændringen lande uden at røre et eneste kaldsted

HotXLS-kolonnebredde-konverteringskæden i Delphi, der føder den målte Max Digit Width af arbejdsbogens Normal-font ind i specifikationsformlen, så en gemt bredde på 8.43 enheder bliver 64 pixels og derefter 48 punkter
Den gemte bredde er et cifferantal, så den målte MDW af Normal-fonten er et input til formlen frem for en styledetalje, og round-trippet 8.43 til 64 til 48 kontrollerer aritmetikken

Hvorfor en Normal-font, der ikke er Calibri, flytter hver grænse

Driften er multiplikativ, hvilket er grunden til, at den ligner en renderingsfejl frem for en enhedsfejl. MDW er en faktor på bredden, ikke et offset. Skub MDW fra 7 til 8, og standardkolonnen på 8.43 enheder går fra 64 pixels til 72, et hop på 8 pixels eller 6 punkter på én kolonne. Ti kolonner af dét, og tabellens højre kant er flyttet det meste af en tomme. Arbejdsbøger, der snubler i dette, er helt almindelige: alt, hvad der genereres af et rapportværktøj, der stampler Arial eller Segoe UI ind i Normal-typografien, alt gemt ud af en ERP-eksportskabelon, alt, hvad en kunde omstylede én gang og glemte

To relaterede layoutsystemer arver fejlen frem for at forårsage den. Flettede områder summerer deres medlemskolonners punktbredder, så en fletning, der passede på én side i Excel, kan flyde over efter MDW-drift, hvilket er værd at huske, når du bygger rapportskabeloner med flettede celler. Shrink-to-fit sammenligner målt tekstbredde med samme kolonnebredde, så den forkerte MDW ændrer også, hvilke celler der skrumper, og hvor meget. Samme familie af enhedsforvirring dukker op i tegneankre, hvor billedgeometri og EMU-skalering har sin egen konverteringskæde at tage fejl på

To HotXLS-kolonnelinealer sammenlignet, én målt med en MDW på 7 pixels og én med 8, som viser, hvordan springet pr. kolonne fra 64 til 72 pixels akkumulerer på tværs af ti kolonner, mens flettede områder og shrink-to-fit arver fejlen
Fordi MDW multiplicerer frem for at forskyde, flytter én forkert måling hver kolonnegrænse, og flettede områder og shrink-to-fit arver driften uden at noget kaster en fejl

Hvordan HotXLS måler MDW ved kørselstid

HotXLS løser MDW ud fra arbejdsbogen selv frem for at antage en konstant, og to procedurer udfører arbejdet. PaginationApplyNormalFont læser Normal-typografiens font af arbejdsbogen og kører i toppen af pagineringsbuilden, før nogen kolonnegeometri beregnes; den nulstiller først til Calibri 11, så en arbejdsbog uden en fonttabel ikke kan arve forældet tilstand fra en tidligere build. Normal-typografiens font er fonts[0] i styles.xml, eksponeret af komponenten som Workbook.Fonts[0]

// Læser fonts[0] (Normal-typografiens font) af regnearkets arbejdsbog.
// Klassiske regneark uden en fonttabel beholder Calibri 11-defaulten.
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;

Den anden procedure, PaginationMeasureMdW, spørger GDI om omfanget af det enkelte tegn '0' gennem GetTextExtentPoint32W på et delt skærmfrit bitmap-canvas, falder tilbage til tmAveCharWidth fra GetTextMetricsW, når extent-kaldet fejler, og falder tilbage til DefaultMDW, når ingen af delene er tilgængelige. Dens cache er én enkelt slot nøglet efter (name, size), hvilket lyder råt, indtil du ser adgangsmønsteret: en pagineringsbuild beder om den samme Normal-font for hver kolonne på hver side, så én slot har en næsten perfekt hit-rate og koster tre sammenligninger pr. kald

Hvad sker der uden fonttabel, uden GUI eller med en manglende font?

HotXLS degraderer til Calibri 11-konstanten i hvert tilfælde, hvor den rigtige Normal-font ikke kan bestemmes, og det gør den stille af design. Klassiske BIFF-regneark er det almindelige tilfælde: de ældre formater bærer ingen XLSX-fontpool for fonts[0] at referere til, så typevogteren exit tidligt, og standard-MDW på 7 står. Det er ikke en fix, det er den tidligere adfærd bevaret bevidst, så at tilføje måling til XLSX-stien ikke kunne lave en regression på output i klassisk format

GDI-afhængigheden er det ærlige forbehold. Måling kører mod en Windows device context, så stien antager en Windows-vært med fonten installeret. I en service eller en headless build-agent løser GDI-tekstmetrikker sig generelt stadig, men en font, der ikke er installeret på den maskine, erstattes af font-mapperen, og du måler erstatningen i stedet. Det fejler aldrig højt; det returnerer et plausibelt tal for den forkerte skrifttype. Hvis server-side-eksporter skal matche en desktopreference, skal du installere de fonte, dine skabeloner navngiver, på eksporthandten eller fastlåse Normal-fonten, inden du kalder regnearkets PDF-eksportsti

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

    // Fastlås Normal-fonten, så den MDW, der måles på denne vært, er den,
    // layoutet blev designet mod, ikke en font-mapper-erstatning.
    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;

Målingscacher, og den der crashede på Win64

Når tekstmåling er en GDI-round-trip frem for en multiplikation, skal den cacheres, og caching inde i et render-pass er der, hvor dette arbejde trak blod. Shrink-to-fit-løkken trapper skriftstørrelsen ned i trin på 0.5 pt og måler igen efter hvert trin, så én celle kan kalde PaginationMeasureTextWidth et dusin gange med den samme streng, og word wrap kalder den igen pr. kandidatlinje. En memo nøglet efter fontnavn, størrelse og tekst skrumper det til ét GDI-kald pr. distinkt streng, gemt i en TStringList som navn/værdi-par

Den anden cache, der blev tilføjet ved siden af, var ikke så pæn. Render-pass 5 løser fontpoolen pr. celle via FontIndex, og dens memo brugte parallelle dynamiske arrays med en håndholdt FontMemoCount. Den første version glemte at kalde ResetFontMemo i starten af hver side, så antallet blev ved med at klatre på tværs af sider, mens arraysene ikke gjorde, og koden skrev forbi enden af dem alle. På Win32 kradser det stille i nabohoben og blev færdig; på Win64 udløste det en access violation ved en skrivning til 0x538 med det samme. Den generaliserbare lære: en array-baseret cache holdt i en variabel på enhedsniveau skal nulstilles ved indgangen til hvert pass, der bruger den, for en strengliste eller ordbog tilgiver en manglende nulstilling ved at vokse, og parallelle arrays gør ikke

Hvordan HotXLS løser arbejdsbogens Normal-font, måler dens Max Digit Width gennem GDI med to fallbacks og cacher resultatet, ved siden af de to render-pass-memoer og den nulstillingsregel, en parallel-array-cache behøver
MDW løses ud fra arbejdsbogen og måles med GDI én gang pr. font og caches derefter efter nøgle, mens render-pass-memoerne viser, hvorfor en parallel-array-cache skal nulstilles ved indgangen til hvert pass

Kontrollér din egen konvertering

Du behøver ikke komponenten for at verificere noget af dette. Tag en arbejdsbog, hvis Normal-font ikke er Calibri 11, læs en bredde fra <col width="..."/> og kør den gennem specifikationsformlen to gange, én gang med MDW 7 og én gang med den MDW, din renderer faktisk måler for den font; hvis svarene er forskellige, og dit output matcher det første, har du fundet driften. Kolonnegeometri er en af de dele af en regnearksmotor, der enten er usynlig eller det eneste, nogen bemærker, og at få den rigtig betyder at behandle Normal-fonten som et input til layout frem for en styledetalje. Hvis du bygger Delphi- eller C++Builder-applikationer, der læser, skriver, renderer og udskriver Excel-arbejdsbøger uden Office installeret, håndterer HotXLS Delphi Excel component MDW-målingen, pagineringsmodellen og PDF-pipelinen bag ét sæt VCL-klasser