Teknisk artikkel

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

Den eksporterte PDF-en plasserer hver kolonnegrense et halvt tegn til venstre for der Excel tegner den, og hver tekstombrydt celle bryter nå et annet sted. Excel-kolonnebredde måles ikke i tegn eller punkter. Den måles i Max Digit Width (MDW)-enheter for arbeidsbokens Normal-font, og HotXLS måler den fonten med GDI før hver pagineringsbygging. Feilmodusen er stille: ingenting kaster, de lagrede breddene går rundt byte for byte, og geometrien er fortsatt noen få prosent av per kolonne til den akkumulerte driften dytter en tabell på én side over på to

Hvilken enhet måles Excel-kolonnebredde i?

En kolonnebredde i et regneark er et antall siffertegn i arbeidsbokens Normal-font, ikke en absolutt måling. ECMA-376 §18.3.1.13 definerer width-attributten til <col> ut fra Maximum Digit Width for den fonten ved 96 dpi, og gir konverteringen fra en lagret bredde tilbake til piksler som et avkuttende uttrykk over MDW. For Calibri 11, som er det Excel leverer som Normal-stil, måler MDW 7 piksler. Kjør standardbredden på 8.43 enheter gjennom spesifikasjonsformelen med MDW 7, og du får nøyaktig 64 piksler, som er 48 punkter ved 96 dpi. Det er tallene Excel selv rapporterer, så de gir en nyttig sjekk: hvis konverteringen din gjenskaper 8.43 enheter til 64 piksler, er aritmetikken riktig og bare MDW-inndataen kan fortsatt være feil

const
  // Maximum digit width (MDW) for standardbruk-fonten i piksler ved 96 dpi.
  // Calibri 11 måler 7 px, som gjenskaper de nøyaktige pikselbreddene
  // Excel lagrer (8.43 enheter -> 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-piksler -> punkter
  if Result < MinimumColumnWidth then
    Result := MinimumColumnWidth;
end;

HotXLS holder den aritmetikken i nøyaktig én funksjon, i lxPagination-enheten, så det finnes ett enkelt sted der målelinjalen kan ta feil. + 5 er utfyllingen Excel legger til for rutenett og celkemarginer, * 0.75 konverterer 96 dpi-piksler til PostScript-punkter, og gulvet ved MinimumColumnWidth finnes slik at en patologisk smal kolonne fortsatt etterlater en stripe gjengiveren kan tegne en kantlinje i. Det offentlige inngangspunktet ColumnWidthToPoints beholder sin gamle ett-argument-signatur og videresender en målt MDW til denne funksjonen, og det er derfor atferdsendringen kunne lande uten å røre ett eneste kallsted

HotXLS-konverteringskjeden for kolonnebredde i Delphi, som mater den målte Max Digit Width til arbeidsbokens Normal-font inn i spesifikasjonsformelen slik at en lagret bredde på 8.43 enheter blir 64 piksler og deretter 48 punkter
Den lagrede bredden er et sifferantall, så den målte MDW-en til Normal-fonten er en inndata til formelen snarere enn en stilartdetalj, og rundturen 8.43 til 64 til 48 sjekker aritmetikken

Hvorfor en Normal-font som ikke er Calibri flytter hver grense

Driften er multiplikativ, og det er derfor den leses som en gjengivelsesfeil snarere enn en enhetsfeil. MDW er en faktor på bredden, ikke en forskyvning. Dytt MDW fra 7 til 8, og standardkolonnen på 8.43 enheter går fra 64 piksler til 72, et hopp på 8 piksler eller 6 punkter på én kolonne. Ti kolonner av det, og høyrekanten på tabellen har flyttet seg mesteparten av en tomme. Arbeidsbøker som utløser dette er helt alminnelige: alt generert av et rapportverktøy som stempler Arial eller Segoe UI inn i Normal-stilen, alt lagret fra en ERP-eksportmal, alt en kunde omstilte én gang og glemte

To beslektede oppsettssystemer arver feilen snarere enn å forårsake den. Sammenslåtte områder summerer punktbreddene til medlemskolonnene sine, så en sammenslåing som fikk plass på én side i Excel kan flyte over etter MDW-drift, noe som er verdt å huske når du bygger rapportmaler med sammenslåtte celler. Shrink-to-fit sammenligner målt tekstbredde med samme kolonnebredde, så feil MDW endrer også hvilke celler som krymper og hvor mye. Den samme familien av enhetsforvirring dukker opp i tegningsankre, der bildegeometri og EMU-skalering har sin egen konverteringskjede å ta feil på

To HotXLS-kolonnelinjaler sammenlignet, én målt med MDW på 7 piksler og én med 8, som viser hvordan hoppet per kolonne fra 64 til 72 piksler akkumuleres over ti kolonner mens sammenslåtte områder og shrink-to-fit arver feilen
Fordi MDW multipliserer snarere enn å forskyve, flytter én feil måling hver kolonnegrense, og sammenslåtte områder og shrink-to-fit arver driften uten at noe kaster

Slik måler HotXLS MDW ved kjøretid

HotXLS løser MDW fra arbeidsboken selv i stedet for å anta en konstant, og to prosedyrer gjør jobben. PaginationApplyNormalFont leser Normal-stil-fonten fra arbeidsboken og kjører øverst i pagineringsbyggingen, før noen kolonnegeometri beregnes; den nullstiller til Calibri 11 først, så en arbeidsbok uten fonttabell kan ikke arve foreldet tilstand fra en tidligere bygging. Normal-stil-fonten er fonts[0] i styles.xml, eksponert av komponenten som Workbook.Fonts[0]

// Leser fonts[0] (Normal-stil-fonten) fra regnearkets arbeidsbok.
// Klassiske regneark uten fonttabell beholder Calibri 11-standarden.
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 andre prosedyren, PaginationMeasureMdW, spør GDI om utstrekningen av det enkelte tegnet '0' gjennom GetTextExtentPoint32W på et delt skjermfritt bitmapped canvas, faller tilbake til tmAveCharWidth fra GetTextMetricsW når utstrekningkallet feiler, og faller tilbake til DefaultMDW når ingen av dem er tilgjengelige. Hurtigbufferen er én enkelt plass nøklet på (name, size), noe som høres røft ut til du ser på tilgangsmønsteret: en pagineringsbygging spør etter samme Normal-font på hver kolonne på hver side, så én plass har en nesten perfekt treffprosent og koster tre sammenligninger per kall

Hva skjer uten fonttabell, uten GUI eller med en manglende font?

HotXLS degraderer til Calibri 11-konstanten i hvert tilfelle der den virkelige Normal-fonten ikke kan bestemmes, og det gjør den stille av design. Klassiske BIFF-regneark er det vanlige tilfellet: de eldre formatene bærer ingen XLSX-fontpool som fonts[0] kan referere til, så typevakten avslutter tidlig og standard-MDW på 7 står. Det er ikke en fiks, det er den tidligere oppførselen bevart med vilje, slik at å legge måling til XLSX-stien ikke kunne forringe utdataene i klassisk format

GDI-avhengigheten er det ærlige forbeholdet. Målingen kjører mot en Windows device context, så stien antar en Windows-vert med fonten installert. I en tjeneste eller en headless byggeagent løser GDI-tekstmetrikk seg vanligvis fortsatt, men en font som ikke er installert på den maskinen blir erstattet av fontmapperen, og du måler erstatningen i stedet. Den feiler aldri høyt; den returnerer et plausibelt tall for feil skriftsnitt. Hvis eksporter på serversiden må matche en skrivebordsreferanse, installer fontene malene dine navngir på verten for eksport, eller fest Normal-fonten før du starter regnearkets PDF-eksportsti

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

    // Fest Normal-fonten slik at MDW-en målt på denne verten er den
    // oppsettet ble designet mot, ikke en fontmapper-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ålehurtigbuffere, og den som krasjet på Win64

Så snart tekstmåling er en GDI-rundtur i stedet for en multiplikasjon, må den hurtiglagres, og hurtiglagring inne i en gjengivelsespass er der dette arbeidet traff en nerve. Shrink-to-fit-løkken trapper skriftstørrelsen ned i trinn på 0.5 pt og måler på nytt etter hvert trinn, så én celle kan kalle PaginationMeasureTextWidth et dusin ganger med samme streng, og ordombryting kaller den igjen per kandidatlinje. Et memo nøklet på fontnavn, størrelse og tekst kollapser det til ett GDI-kall per distinkt streng, lagret i en TStringList som navn/verdi-par

Den andre hurtigbufferen som ble lagt til ved siden av, var ikke like ryddig. Gjengivelsespass 5 løser fontpoolen per celle via FontIndex, og memoet brukte parallelle dynamiske arrayer med en håndholdt FontMemoCount. Den første versjonen glemte å kalle ResetFontMemo ved starten av hver side, så antallet fortsatte å klatre på tvers av sider mens arrayene ikke gjorde det, og koden skrev forbi slutten på alle sammen. På Win32 kruset det stille i nabohaugen og ble ferdig; på Win64 utløste det en access violation ved et skriv til 0x538 umiddelbart. Den generaliserbare lærdommen: en array-basert hurtigbuffer holdt i en enhetsnivåvariabel må nullstilles ved inngangen til hver pass som bruker den, fordi en strengliste eller ordbok tilgir en manglende nullstilling ved å vokse, og parallelle arrayer gjør ikke det

Slik løser HotXLS arbeidsbokens Normal-font, måler dens Max Digit Width gjennom GDI med to fallbacks, og hurtiglagrer resultatet, ved siden av de to gjengivelsespassememoene og nullstillingsregelen en parallel-array-hurtigbuffer trenger
MDW løses fra arbeidsboken og måles med GDI én gang per font, og hurtiglagres deretter etter nøkkel, mens gjengivelsespassememoene viser hvorfor en parallel-array-hurtigbuffer må nullstilles ved inngangen til hver pass

Sjekke din egen konvertering

Du trenger ikke komponenten for å verifisere noe av dette. Ta en arbeidsbok hvis Normal-font ikke er Calibri 11, les en bredde fra <col width="..."/>, og kjør den gjennom spesifikasjonsformelen to ganger, én gang med MDW 7 og én gang med MDW-en gjengiveren din faktisk måler for den fonten; hvis svarene er forskjellige og utdataene dine matcher det første, har du funnet driften. Kolonnegeometri er en av de delene av en regnearkmotor som enten er usynlig eller det eneste noen legger merke til, og å få den riktig betyr å behandle Normal-fonten som en inndata til oppsettet snarere enn en stilartdetalj. Hvis du bygger Delphi- eller C++Builder-applikasjoner som leser, skriver, gjengir og skriver ut Excel-arbeidsbøker uten Office installert, håndterer HotXLS Delphi Excel-komponenten MDW-målingen, pagineringsmodellen og PDF-pipelinen bak ett sett med VCL-klasser