Tehnični članak

Implementacija odložiščnega formata CF_HTML v Delphiju

Kopirate obseg iz mreže Delphi in ga prilepite v Word, oblikovanje pa običajno izgine: navadno besedilo, brez krepkih glav, robov in polnil. HotXLS to vrzel zapolni z TXLSRange.CopyToClipboard, ki na odložišče poleg navadnega besedila Unicode postavi tudi odložiščni paket CF_HTML — obliko sistema Windows za oblikovani HTML z označbami fragmenta, natančnimi do bajta

To zveni preprosto, dokler ne pogledate, kaj paket CF_HTML dejansko zahteva. Oblika potrebuje kratko besedilno glavo, ki natančno poimenuje, kje se fragment začne in konča znotraj večjega medpomnilnika odložišča, ti položaji pa so odmiki bajtov, šteti glede na večbajtno kodiranje, v katerem se na koncu znajde HTML. Če aritmetiko zgrešite že za en bajt, ciljna aplikacija zajame napačen del označb ali odneha in uporabi navadno besedilo, pri čemer nobena od teh napak ni videti kot napaka v vaši kodi — videti je kot Word, ki pač deluje po svoje

Zakaj kopiranje in lepljenje iz mreže Delphi običajno izgubi oblikovanje

Privzeti klic odložišča sistema Windows, po katerem poseže večina kode Delphi, SetClipboardData s CF_TEXT ali CF_UNICODETEXT, prenaša samo navadne znake, zato oblikovanje iz izvorne mreže nima kam. Word, Outlook in vsi brskalniki na osnovi Chromiuma pri lepljenju iščejo bogatejšo obliko: predstavitev izbora v HTML, skupaj z vstavljenimi slogi, strukturo tabele in povezavami. Tudi Excel se zanaša na popolnoma enak pristop — ko kopirate obseg v Excelu, odložišče tiho prejme več oblik hkrati, med njimi HTML, zato aplikacija, v katero lepite, izbere najbogatejšo obliko, ki jo razume. Komponenta, ki zapisuje samo CF_UNICODETEXT, vsem tem bogatejšim odjemalcem ne ponudi ničesar, s čimer bi lahko delali, zato vizualnega bogastva, ki ga je uporabnik pravkar kopiral, preprosto ni mogoče prilepiti

Kaj točno je odložiščni format CF_HTML

CF_HTML ni nespremenljiv sistemski format odložišča, kot je CF_TEXT; registrira se dinamično in zahteva po imenu prek RegisterClipboardFormat('HTML Format'), njegov paket pa sestavljata kratka glava ASCII in dokument ali fragment HTML. Glava vsebuje pet polj — Version, StartHTML, EndHTML, StartFragment, EndFragment — pri čemer je Version vedno 0.9, druge štiri vrednosti pa so decimalna števila, zapisana z znaki ASCII. StartHTML in EndHTML omejujeta celoten dokument, kot naj ga prejemna aplikacija razčleni za kontekst, vključno s pisavami in slogi, medtem ko StartFragment in EndFragment omejujeta ožji izsek, ki dejansko pristane na mestu kazalca; ta je običajno v sami označbi označen s komentarjema <!--StartFragment--> in <!--EndFragment-->, da meje preživijo naivno ponovno serializacijo

Odmiki bajtov, ne število znakov: klasična past CF_HTML

Štiri številska polja glave CF_HTML so odmiki bajtov v natančnem zaporedju bajtov na odložišču, šteti od prvega znaka same glave — ne gre za število znakov, kodne točke Unicode ali odmike glede na fragment oziroma oznako <body>. Prav ta razlika povzroči tihe napake v ročno izdelanih izvedbah CF_HTML: Length v Delphijevem UnicodeString vrne število kodnih enot UTF-16, kar je pri navadnem besedilu ASCII po naključju enako številu bajtov, zato napaka neopazno prestane vsak preizkus z angleškimi vzorčnimi podatki in se pokaže šele, ko kopirana celica vsebuje pomišljaj em, valutni simbol ali naglašeni znak — znak za evro je ena kodna enota UTF-16, vendar trije bajti v UTF-8, zato se vsak odmik, izračunan za to točko, zamakne za število dodatnih bajtov, ki jih je dodalo kodiranje. Posledica ni sesutje; prejemna aplikacija zaseže natančen obseg bajtov, na katerega kaže glava, najde izsek označb, ki se začne ali konča sredi oznake, nato izriše neuporabne podatke ali odneha in tiho uporabi navadno besedilo ob njem na odložišču, brez česar koli v vaši kodi, kar bi pojasnilo razlog — takšna je oblika kode, ki ustvari prav to napako:

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

Kako HotXLS ohrani natančnost glave do bajta

HotXLS se tej vrsti napake izogne že s samo zgradbo: TXLSRange.CopyToClipboard in enota lxClipboard pod njo v celoti izdelata dokument CF_HTML in njegovo glavo kot AnsiString, Delphijev tip bajtnega niza, zato Length in Pos povsod v izračunu že vračata položaje bajtov — ni ločenega koraka in s tem tudi ne koraka, ki bi ga lahko pozabili, pri katerem bi bilo treba število znakov Unicode pretvoriti v število bajtov, preden se zapiše v glavo

Če kdaj ročno izdelujete glavo CF_HTML, je vredno poznati še en manjši trik. Glava se zapiše dvakrat: prvič z desetimi ničlami namesto vsakega od štirih odmikov, da lahko izmerimo njeno lastno dolžino v bajtih, nato pa še enkrat z vstavljenimi pravimi odmiki. Ker je vsak pravi odmik oblikovan z enako fiksno širino desetih mest, je druga glava po dolžini bajt za bajtom enaka različici z ogradami, zato prejšnja meritev po prepisu ostane veljavna. Če fiksno širino preskočite in namesto tega število oblikujete z navadnim IntToStr, se lahko glava med obema prehodoma skrajša ali podaljša za eno mesto in tiho razveljavi vsak naslednji odmik:

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;

Zakaj mora biti paket navadnega besedila še vedno priložen

TXLSRange.CopyToClipboard na odložišče nikoli ne postavi samo CF_HTML; v istem klicu vedno zapiše tudi CF_UNICODETEXT, ker je CF_HTML registriran format in ne eden od fiksnih konstant CF_*, ki jih vsaka aplikacija sistema Windows že zna iskati — urejevalnik navadnega besedila, starejša mreža ali karkoli, kar nikoli ni preverjalo 'HTML Format', ga sploh ne bo videlo, zato kopirani obseg prispe kot besedilo, ločeno s tabulatorji, ali pa sploh ne prispe. To tabulatorsko besedilo tudi ni grob približek: celice s formulami kopirajo niz formule, pri čemer se na začetku obnovi =, če ga shranjeno besedilo ne vsebuje, skladno z vedenjem Excelovega besedila na odložišču, navadne celice kopirajo svoj FormattedText — niz, kot je prikazan, zato se valutna celica kopira kot $1,234.56 in ne kot osnovni 1234.56 — vsako polje s tabulatorjem, narekovajem ali prelomom vrstice pa se zapiše v narekovajih, notranji narekovaji pa se podvojijo, enako kot pri zapisu CSV

SaveAsHTML ni ločena pot izrisovanja, dodana samo za primer odložišča. CopyToClipboard pokliče istega zapisovalnika HTML, opisanega v izvozu CSV, TSV in HTML za HotXLS, nato pa njegov rezultat ovije v ovojnico CF_HTML, namesto da bi ga shranil kot samostojno datoteko, zato se vse, kar velja za ta HTML, neposredno prenese v vsebino na odložišču. Pridobitev obsega delovnega lista v obeh oblikah z enim klicem je videti tako:

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;

Ali prilepljeni obseg ohrani pisave, barve in združene celice

Da, ker je HTML-del paketa popoln izris obsega in ne le izvoz podatkov: pisave, barve polnil, robovi, številčni formati in združene celice se prenesejo kot vstavljeni slogi in struktura tabele, z uporabo istega mehanizma oblikovanja, opisanega v vodniku HotXLS za pogojno oblikovanje in obogateno besedilo, saj se tako obogateno besedilo celice kot rezultat pogojnega oblikovanja podajata v isti izris, iz katerega bere CopyToClipboard. Kar se med prenosom ne ohrani, je vedenje živih formul: besedilna oblika celice s formulo vsebuje niz formule, zato bi ga cilj, ki razume preglednice, načeloma lahko znova izračunal, vendar HTML-oblika vedno vsebuje samo zadnji izračunani rezultat, saj HTML nima pojma o formuli, ki bi jo lahko ovrednotil brskalnik ali urejevalnik besedil

Preverjanje lepljenja in obravnava zasedenega odložišča

Dve navadi odkrijeta večino težav z odložiščem, še preden jih opazi stranka. Najprej prilepite v Beležnico in preverite, ali je rezervna vsebina CF_UNICODETEXT smiselno besedilo, ločeno s tabulatorji, nato isti izvod prilepite v Word ali brskalnik in preverite, ali se prikaže oblikovana različica — paket, ki je v eni aplikaciji videti prav, v drugi pa napačno, običajno pomeni, da sta označbi fragmenta pristali na napačnem mestu. Nato rezultat tipa Boolean, ki ga vrne CopyToClipboard, obravnavajte kot pomembnega in ne kot okras: OpenClipboard lahko spodleti, ko ima drug proces odprto odložišče, kar je na zasedenem namizju dovolj pogosto, da bo nekontroliran klic nekoč prilepil prazno vsebino brez napake, ki bi pojasnila razlog, pred čimer ščiti spodnji vzorec ponavljanja:

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;

Sama oblika ni nenavadna, ko sta glava natančna do bajta in rezervno navadno besedilo pošteno glede svoje vsebine — odkar jo je prvič določil Internet Explorer, je večinoma nespremenjena in vse večje aplikacije sistema Windows jo še vedno berejo enako. CopyToClipboard je poleg PasteFromClipboard, bralne strani iste izmenjave, del širšega nabora odložišča in izvoza, opisanega na strani izdelka komponente HotXLS