Teknisk artikkel

Å implementere CF_HTML-utklippstavleformatet i Delphi

Kopier et område ut av et Delphi-rutenett og lim det inn i Word, og formateringen forsvinner vanligvis: ren tekst, ingen fete overskrifter, ingen kantlinjer, ingen fyllinger. HotXLS lukker det gapet med TXLSRange.CopyToClipboard, som legger en CF_HTML-utklippstavle-payload — Windows-formatet for stilsatt HTML med byte-eksakte fragment-markører — på utklippstavlen ved siden av ren Unicode-tekst

Det høres enkelt ut inntil man ser hva en CF_HTML-payload faktisk krever. Formatet trenger en kort tekst-header som navngir nøyaktig hvor fragmentet begynner og slutter inne i den større utklippstavle-bufferen, og de posisjonene er byte-forskyvninger, telt gjennom hvilken som helst multi-byte-koding HTML-en ender opp i. Få aritmetikken feil med bare én byte, og målapplikasjonen enten griper feil skive av markup eller gir opp og faller tilbake til ren tekst, og ingen av feilene ser ut som en bug i koden din — det ser ut som Word som er Word

Hvorfor kopier-lim-inn fra et Delphi-rutenett vanligvis mister formateringen

Standard Windows-utklippstavlekallet det meste Delphi-kode griper til, SetClipboardData med CF_TEXT eller CF_UNICODETEXT, bærer bare ren tekst noensinne, slik at all stiling anvendt i kilderutenettet ikke har noe sted å gå. Word, Outlook, og hver Chromium-basert nettleser ser etter et rikere format når man limer inn: en HTML-representasjon av utvalget, komplett med inline-stiler, tabellstruktur, og lenker. Excel selv er avhengig av nettopp dette trikset — kopier et område i Excel, og utklippstavlen mottar stille flere formater på én gang, HTML blant dem, slik at hvilken applikasjon man enn limer inn i, plukker den rikeste den forstår. En komponent som bare noensinne skriver CF_UNICODETEXT, gir hver eneste av de rikere konsumentene ingenting å jobbe med, og den visuelle rikdommen brukeren nettopp kopierte, er rett og slett ikke der å lime inn

Hva er egentlig CF_HTML-utklippstavleformatet?

CF_HTML er ikke et fast systemutklippstavleformat som CF_TEXT; det er et dynamisk registrert et, forespurt ved navn gjennom RegisterClipboardFormat('HTML Format'), og payloaden dens er en kort ASCII-header etterfulgt av et HTML-dokument eller -fragment. Headeren bærer fem felt — Version, StartHTML, EndHTML, StartFragment, EndFragment — der Version alltid er 0.9 og de andre fire er desimaltall skrevet ut som ASCII-siffer. StartHTML og EndHTML avgrenser hele dokumentet slik mottakerapplikasjonen bør parse det for kontekst, skrifttyper og stiler inkludert, mens StartFragment og EndFragment avgrenser den snevrere skiven som faktisk havner ved markøren, konvensjonelt markert i selve markupen med <!--StartFragment-->- og <!--EndFragment-->-kommentarer slik at grensene overlever naiv re-serialisering

Byte-forskyvninger, ikke tegnantall: den klassiske CF_HTML-fellen

CF_HTMLs fire numeriske header-felt er byte-forskyvninger inn i den eksakte bytesekvensen som ligger på utklippstavlen, telt fra det aller første tegnet i selve headeren — ikke tegnantall, ikke Unicode-kodepunkter, og ikke forskyvninger relativt til fragmentet eller <body>-taggen. Det skillet er der håndbygde CF_HTML-implementasjoner stille går galt: en Delphi UnicodeStrings Length rapporterer UTF-16-kodeenheter, noe som tilfeldigvis tilsvarer bytetallet for ren ASCII-tekst, så bugen glir ren gjennom enhver test skrevet med engelske eksempeldata og viser seg først når en kopiert celle inneholder en tankestrek, et valutasymbol, eller en aksentert bokstav — et eurotegn er én UTF-16-kodeenhet, men tre byte i UTF-8, og hver forskyvning beregnet etter det punktet driver med hvor mange ekstra byte kodingen la til. Feilen som følger, er ikke et krasj; det er mottakerapplikasjonen som griper den eksakte byte-rekkevidden headeren pekte på, finner en skive av markup som starter eller slutter midt i en tag, og enten gjengir søppel eller gir opp og faller tilbake til hvilken ren tekst som enn ligger ved siden av den på utklippstavlen, stille, uten noe i koden din som forklarer hvorfor — her er formen på kode som produserer nøyaktig den feilen:

// Fragile: Length() on a UnicodeString counts UTF-16 code units, not bytes
var
  Header: string;
  Fragment: string;
  StartFragmentOfs: Integer;
begin
  Header := 'Version:0.9'#13#10 + 'StartHTML:0000000000'#13#10 + '...';
  StartFragmentOfs := Length(Header) + Pos('<!--StartFragment-->', Fragment);
  // A currency symbol, an em dash, or any accented character placed
  // before this point costs one character here but two or three bytes
  // once the document is UTF-8 encoded, so StartFragmentOfs now points
  // short of where the fragment actually begins on the real clipboard
end;

Hvordan HotXLS holder headeren byte-nøyaktig

HotXLS unngår denne klassen av bug strukturelt: TXLSRange.CopyToClipboard og lxClipboard-enheten under den bygger CF_HTML-dokumentet og headeren dets fullstendig som AnsiString, Delphis byte-strengtype, slik at Length og Pos allerede returnerer byteposisjoner overalt i beregningen — det finnes ikke noe separat trinn, og derfor intet trinn å glemme, der et Unicode-tegnantall ville trenge å konverteres til et bytetall før det går inn i headeren

Det finnes et andre, mindre triks verdt å kjenne til hvis man noensinne bygger en CF_HTML-header for hånd. Headeren skrives to ganger: én gang med ti null-siffer som stedfortreder for hver av de fire forskyvningene, slik at dens egen bytelengde kan måles, og én gang til med de reelle forskyvningene lappet inn. Fordi hver reell forskyvning formateres til den samme faste ti-sifrede bredden, kommer den andre headeren ut byte-for-byte samme lengde som stedfortreder-versjonen, noe som er nøyaktig grunnen til at den tidligere målingen forblir gyldig etter omskrivingen. Hopp over den faste bredden, formater et tall med en vanlig IntToStr i stedet, og headeren kan krympe eller vokse med et siffer mellom de to passeringene, og stille ugyldiggjøre hver forskyvning som følger den:

const
  Placeholder = '0000000000';   // 10 ASCII digits: fixed width in, fixed width out
var
  Header: AnsiString;           // AnsiString.Length is a byte count, not a char count
  StartHtmlOfs: Integer;
begin
  Header := 'Version:0.9'#13#10 +
    'StartHTML:' + Placeholder + #13#10 +
    'EndHTML:' + Placeholder + #13#10 +
    'StartFragment:' + Placeholder + #13#10 +
    'EndFragment:' + Placeholder + #13#10;
  StartHtmlOfs := Length(Header);   // safe to measure once, up front
  // ...compute the real offsets against the AnsiString document...
  // then rebuild Header with the real numbers formatted to the same
  // 10-digit width, so its byte length -- and therefore StartHtmlOfs --
  // never moves between the placeholder pass and the final one
end;

Hvorfor ren-tekst-payloaden fortsatt må bli med

TXLSRange.CopyToClipboard plasserer aldri CF_HTML alene på utklippstavlen; den skriver alltid CF_UNICODETEXT i det samme kallet, fordi CF_HTML er et registrert format snarere enn en av de faste CF_*-konstantene hver Windows-applikasjon allerede vet å se etter — en ren tekstredigerer, et eldre rutenett, eller hva som helst som aldri sjekket etter 'HTML Format', vil ikke se det i det hele tatt, og området du kopierte, ankommer enten som tabulator-avgrenset tekst eller det ankommer ikke. Den tabulator-avgrensede teksten er heller ikke en grov tilnærming: formelceller kopieres som sin formelstreng med et innledende = gjenopprettet hvis den lagrede teksten droppet det, i tråd med hvordan Excels egen utklippstavle-tekst oppfører seg, vanlige celler kopierer sin FormattedText — strengen slik den vises, slik at en valutacelle kopieres som $1,234.56, ikke den underliggende 1234.56 — og ethvert felt som inneholder en tabulator, et anførselstegn, eller et linjeskift, blir sitert med innebygde anførselstegn doblet, den samme konvensjonen CSV bruker

SaveAsHTML er ikke en separat gjengivelsesvei boltet på bare for utklippstavle-tilfellet. CopyToClipboard kaller den nøyaktig samme HTML-skriveren beskrevet i HotXLS' CSV-, TSV-, og HTML-eksport, og pakker deretter inn hva den skriveren enn produserer i CF_HTML-konvolutten i stedet for å lagre det som en frittstående fil, slik at alt som er sant om den HTML-en, bæres rett gjennom til det som havner på utklippstavlen. Å hente et regnearkområde sammen som begge formater i ett kall ser slik ut:

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-report.xlsx');
    // Classic TXLSWorkbook ranges expose the identical method as
    // Workbook.Sheets[1].Range['A1', 'F40'].CopyToClipboard
    if Book.Sheets[1].Range['A1:F40'].CopyToClipboard then
      ShowMessage('Range copied - press Ctrl+V in Word or a browser')
    else
      ShowMessage('Clipboard was busy; see the retry pattern below');
  finally
    Book.Free;
  end;
end;

Beholder det innlimte området sine skrifttyper, farger, og sammenslåtte celler?

Ja, fordi HTML-halvparten av payloaden er en full gjengivelse av området, ikke en bar datadump: skrifttyper, fyllfarger, kantlinjer, tallformater, og sammenslåtte celler kommer alle gjennom som inline-stiler og tabellstruktur, det samme stilings-maskineriet dekket i HotXLS' guide til betinget formatering og rik tekst, ettersom en celles rik-tekst-forløp og betinget-formaterings-resultat begge mater den samme gjengivelsen CopyToClipboard leser fra. Det som ikke overlever turen, er levende-formel-oppførsel: en formelcelles rentekst-form bærer formelstrengen, slik at et regneark-bevisst innlimingsmål i prinsippet kunne beregne den på nytt, men HTML-formen bærer bare noensinne det siste beregnede resultatet, fordi HTML ikke har noe konsept om en formel for en nettleser eller tekstbehandler å evaluere

Å verifisere innlimingen, og håndtere en opptatt utklippstavle

To vaner fanger de fleste utklippstavleproblemer før en kunde gjør det. Lim inn i Notisblokk først for å bekrefte at CF_UNICODETEXT-reserveløsningen er fornuftig tabulator-avgrenset tekst, lim deretter inn den samme kopien i Word eller en nettleser for å bekrefte at den stilsatte versjonen dukker opp — en payload som ser riktig ut i den ene og feil ut i den andre, betyr vanligvis at fragment-markørene havnet feil sted. Behandle deretter det boolske resultatet CopyToClipboard returnerer som meningsfullt, ikke dekorativt: OpenClipboard kan feile når en annen prosess holder utklippstavlen åpen, vanlig nok på et travelt skrivebord til at ett usjekket kall til slutt limer inn ingenting uten noen feil til å forklare hvorfor, noe retry-en nedenfor beskytter mot:

function TryCopyRangeToClipboard(Workbook: TXLSXWorkbook): Boolean;
var
  Attempt: Integer;
begin
  Result := False;
  for Attempt := 1 to 5 do
  begin
    Result := Workbook.Sheets[1].Range['A1:F40'].CopyToClipboard;
    if Result then
      Break;
    Sleep(50);   // give whichever app is holding the clipboard a moment
  end;
  if not Result then
    raise Exception.Create('Could not take ownership of the clipboard');
end;

Formatet selv er ikke eksotisk når headeren først er byte-nøyaktig og ren-tekst-reserveløsningen er ærlig om hva den inneholder — det har eksistert stort sett uendret siden Internet Explorer først definerte det, og hver stor Windows-applikasjon leser det fortsatt på samme måte. CopyToClipboard sitter ved siden av PasteFromClipboard, lesesiden av den samme utvekslingen, i den bredere utklippstavle- og eksport-overflaten dokumentert på HotXLS-komponentens produktside