Tehnički članak

Implementacija CF_HTML formata međuspremnika u Delphiju

Kada kopirate opseg iz Delphi mreže i nalepite ga u Word, formatiranje obično nestane: ostane običan tekst, bez podebljanih zaglavlja, ivica i ispuna. HotXLS rešava taj problem pomoću TXLSRange.CopyToClipboard, koji u međuspremnik upisuje CF_HTML sadržaj — Windows format za HTML sa stilovima i markerima fragmenta čije su pozicije precizno određene u bajtovima — zajedno sa običnim Unicode tekstom

To zvuči jednostavno dok ne pogledate šta CF_HTML sadržaj zaista zahteva. Format ima kratko tekstualno zaglavlje koje tačno navodi gde fragment počinje i završava se unutar većeg međuspremničkog bafera, a te pozicije su pomeraji u bajtovima izračunati kroz višebajtno kodiranje u kojem HTML na kraju postoji. Ako aritmetiku pogrešite makar za jedan bajt, ciljna aplikacija preuzima pogrešan deo oznaka ili odustaje i vraća se na običan tekst, a nijedan od tih ishoda ne izgleda kao greška u vašem kodu — više liči na to da Word radi ono što Word radi

Zašto kopiranje i lepljenje iz Delphi mreže obično gubi formatiranje

Podrazumevani Windows poziv za međuspremnik kojem većina Delphi koda pribegava, SetClipboardData sa CF_TEXT ili CF_UNICODETEXT, prenosi samo obične znakove, pa stilovi primenjeni u izvornoj mreži nemaju gde da odu. Word, Outlook i svaki pregledač zasnovan na Chromiumu pri lepljenju traže bogatiji format: HTML prikaz izbora sa ugrađenim stilovima, strukturom tabele i vezama. I sam Excel se oslanja upravo na ovaj trik — kada kopirate opseg u Excelu, međuspremnik neprimetno dobije više formata odjednom, uključujući HTML, pa aplikacija u koju lepite bira najbogatiji format koji razume. Komponenta koja upisuje samo CF_UNICODETEXT ne daje nijednom od tih bogatijih potrošača šta da obradi, pa vizuelno bogatstvo koje je korisnik upravo kopirao više ne postoji za lepljenje

Šta je zapravo CF_HTML format međuspremnika

CF_HTML nije fiksni sistemski format međuspremnika poput CF_TEXT; to je dinamički registrovan format koji se traži po imenu pomoću RegisterClipboardFormat('HTML Format'), a njegov sadržaj čine kratko ASCII zaglavlje i HTML dokument ili fragment. Zaglavlje sadrži pet polja — Version, StartHTML, EndHTML, StartFragment i EndFragment — pri čemu je Version uvek 0.9, dok su preostala četiri polja decimalni brojevi zapisani ASCII ciframa. StartHTML i EndHTML obuhvataju ceo dokument koji prijemna aplikacija treba da obradi zbog konteksta, uključujući fontove i stilove, dok StartFragment i EndFragment obuhvataju uži deo koji zaista stiže na mesto kursora, uobičajeno označen komentarima <!--StartFragment--> i <!--EndFragment--> u samim oznakama kako bi granice preživele naivnu ponovnu serijalizaciju

Dijagram CF_HTML sadržaja clipboard-a izgrađenog od HotXLS u Delphi-ju, koji pokazuje petopoljni ASCII zaglavlje i UTF-8 dokument sa StartFragment i EndFragment komentar markerima
CF_HTML omot je kratko ASCII zaglavlje ispred UTF-8 dokumenta, sa nalepljenim isečkom označenim StartFragment i EndFragment komentarima

Pomeraji u bajtovima, a ne broj znakova: klasična zamka CF_HTML formata

Četiri brojčana polja CF_HTML zaglavlja predstavljaju pomeraje u bajtovima kroz tačan niz bajtova koji se nalazi u međuspremniku, računato od prvog znaka samog zaglavlja — to nisu brojevi znakova, Unicode kodne tačke niti pomeraji u odnosu na fragment ili oznaku <body>. Tu ručno pisane CF_HTML implementacije najčešće neprimetno greše: Length nad Delphi tipom UnicodeString vraća broj UTF-16 kodnih jedinica, što slučajno odgovara broju bajtova za običan ASCII tekst, pa greška bez problema prođe svaki test napisan sa engleskim probnim podacima i pokaže se tek kada kopirana ćelija sadrži dugu crtu, simbol valute ili znak sa dijakritikom — znak evra zauzima jednu UTF-16 kodnu jedinicu, ali tri bajta u UTF-8, pa svaki pomeraj izračunat posle te tačke sklizne za broj dodatnih bajtova koje je kodiranje unelo. Posledica nije pad programa, već prijemna aplikacija uzima tačan opseg bajtova na koji je zaglavlje pokazalo, pronalazi deo oznaka koji počinje ili se završava usred taga i ili prikazuje neispravan sadržaj ili odustaje i nečujno prelazi na običan tekst pored njega, bez ičega u vašem kodu što bi objasnilo zašto — ovakav oblik koda proizvodi upravo taj problem:

// Krhko: Length() nad UnicodeString broji UTF-16 kod jedinice, ne bajtove
var
  Header: string;
  Fragment: string;
  StartFragmentOfs: Integer;
begin
  Header := 'Version:0.9'#13#10 + 'StartHTML:0000000000'#13#10 + '...';
  StartFragmentOfs := Length(Header) + Pos('<!--StartFragment-->', Fragment);
  // Simbol valute, em crta ili bilo koji akcentovan karakter postavljen
  // pre ove tačke ovde košta jedan karakter, ali dva ili tri bajta
  // kada se dokument enkoduje u UTF-8, pa StartFragmentOfs sada pokazuje
  // pre mesta gde fragment zaista počinje na pravom klipbordu
end;

Kako HotXLS održava tačnost zaglavlja na nivou bajtova

HotXLS strukturno izbegava ovu klasu grešaka: TXLSRange.CopyToClipboard i jedinica lxClipboard koja se nalazi ispod nje grade CF_HTML dokument i njegovo zaglavlje u potpunosti kao AnsiString, Delphi tip niske koji predstavlja niz bajtova, pa Length i Pos svuda u računu već vraćaju pozicije u bajtovima — nema posebnog koraka koji bi mogao da se zaboravi, u kojem bi broj Unicode znakova trebalo pretvoriti u broj bajtova pre upisivanja u zaglavlje

Dijagram koji suprotstavlja broj UTF-16 kodnih jedinica sa UTF-8 bajt ofsetima u Delphi CF_HTML zaglavlju, gde znaci sa akcentima i znak eura odvajaju granice fragmenta
Jedan višebajtni znak pomera svaki bajtni pomak izračunat posle njega, pa HotXLS meri celo zaglavlje u AnsiString bajtovima umesto kodnih jedinica

Postoji još jedan manji trik koji vredi znati ako ikada ručno pravite CF_HTML zaglavlje. Zaglavlje se upisuje dva puta: prvi put sa deset nula kao zamenom za svako od četiri pomeraja, kako bi se izmerila njegova sopstvena dužina u bajtovima, a zatim ponovo sa stvarnim pomerajima. Pošto je svaki stvarni pomeraj formatiran na istu fiksnu širinu od deset cifara, drugo zaglavlje ima potpuno istu dužinu u bajtovima kao verzija sa rezervisanim mestima, upravo zato što ranije merenje ostaje važeće posle izmene. Ako preskočite fiksnu širinu i umesto toga broj formatirate običnim IntToStr, zaglavlje se između dva prolaza može skratiti ili produžiti za jednu cifru i nečujno pokvariti svaki pomeraj koji sledi:

const
  Placeholder = '0000000000';   // 10 ASCII cifara: fiksna širina na ulazu, fiksna na izlazu
var
  Header: AnsiString;           // AnsiString.Length je broj bajtova, ne broj karaktera
  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);   // bezbedno se meri jednom, unapred
  // ...izračunajte prave ofsete u odnosu na AnsiString dokument...
  // zatim ponovo izgradite Header sa pravim brojevima formatiranim na istu
  // širinu od 10 cifara, tako da se njegova dužina u bajtovima -- a time i StartHtmlOfs --
  // nikad ne pomera između prolaza sa placeholderom i konačnog prolaza
end;

Zašto sadržaj običnog teksta i dalje mora da ide zajedno

TXLSRange.CopyToClipboard nikada ne postavlja samo CF_HTML u međuspremnik; u istom pozivu uvek upisuje i CF_UNICODETEXT, jer je CF_HTML registrovani format, a ne jedna od fiksnih konstanti CF_* koje svaka Windows aplikacija već zna da traži — običan uređivač teksta, stara mreža ili bilo šta što nikada nije proverilo 'HTML Format' uopšte ga neće videti, pa kopirani opseg ili stiže kao tekst razdvojen tabulatorima ili ne stiže. Taj tekst razdvojen tabulatorima nije ni približna zamena: ćelije sa formulama kopiraju se kao tekst formule sa vraćenim početnim znakom = ako ga sačuvani tekst nije sadržao, u skladu sa ponašanjem Excelovog tekstualnog međuspremnika, obične ćelije kopiraju svoj FormattedText — tekst onako kako je prikazan, pa se novčana ćelija kopira kao $1,234.56, a ne kao osnovna vrednost 1234.56 — dok se svako polje koje sadrži tabulator, navodnik ili prelom reda stavlja pod navodnike, a unutrašnji navodnici udvostručuju, po istoj konvenciji koju koristi CSV

Dijagram HotXLS CopyToClipboard koji piše CF_HTML i CF_UNICODETEXT na Windows clipboard tako Word i pregledači nalepe stilizovane tabele dok obični editori prime tekst razdvojen tabovima
CopyToClipboard uvek piše pola čistog teksta pored HTML-a, pa svaki cilj od Word do Notepad prima nešto pošteno

SaveAsHTML nije odvojeni put prikaza naknadno dodat samo za slučaj međuspremnika. CopyToClipboard poziva potpuno isti HTML zapisivač opisan u HotXLS izvozu CSV, TSV i HTML sadržaja, a zatim sve što taj zapisivač proizvede umotava u CF_HTML omotač umesto da ga sačuva kao samostalnu datoteku, pa se sve što važi za taj HTML prenosi direktno na sadržaj koji dospeva u međuspremnik. Preuzimanje opsega radnog lista u oba formata jednim pozivom izgleda ovako:

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-report.xlsx');
    // Klasični TXLSWorkbook opsezi izlažu identičan metod kao
    // 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;

Da li nalepjeni opseg zadržava fontove, boje i spojene ćelije

Da, zato što je HTML deo sadržaja potpuni prikaz opsega, a ne goli izvoz podataka: fontovi, boje ispune, ivice, formati brojeva i spojene ćelije prenose se kao ugrađeni stilovi i struktura tabele, istim mehanizmom stilizovanja koji je obrađen u HotXLS vodiču za uslovno formatiranje i obogaćeni tekst, jer i rasponi obogaćenog teksta ćelije i rezultat uslovnog formatiranja ulaze u isti prikaz koji CopyToClipboard koristi. Ono što ne preživljava prenos jeste ponašanje živih formula: tekstualni oblik ćelije formule nosi tekst formule, pa bi odredište koje razume radne tabele u načelu moglo ponovo da je izračuna, ali HTML oblik nosi samo poslednji izračunati rezultat, jer HTML nema pojam formule koju bi pregledač ili program za obradu teksta mogao da izračuna

Provera lepljenja i postupanje sa zauzetim međuspremnikom

Dve navike otkrivaju većinu problema sa međuspremnikom pre nego što ih otkrije korisnik. Najpre nalepite u Notepad da potvrdite da je rezerva CF_UNICODETEXT ispravan tekst razdvojen tabulatorima, a zatim isti sadržaj nalepite u Word ili pregledač da potvrdite da se prikazuje stilizovana verzija — sadržaj koji izgleda ispravno u jednoj aplikaciji, a pogrešno u drugoj, obično znači da su markeri fragmenta završili na pogrešnom mestu. Zatim rezultat tipa Boolean koji vraća CopyToClipboard tretirajte kao važan podatak, a ne kao ukras: OpenClipboard može da ne uspe kada drugi proces drži međuspremnik otvorenim, što je dovoljno često na zauzetom računaru da jedan neproveren poziv na kraju ne nalepi ništa bez greške koja bi objasnila zašto, a upravo to štiti obrazac ponovnog pokušaja u nastavku:

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);   // dajte trenutak aplikaciji koja trenutno drži klipbord
  end;
  if not Result then
    raise Exception.Create('Could not take ownership of the clipboard');
end;

Sam format nije naročito neobičan kada su zaglavlje tačno u bajtovima, a rezerva običnog teksta iskrena u vezi sa sadržajem koji nosi — uglavnom je nepromenjen još od trenutka kada ga je Internet Explorer prvi put definisao, a sve glavne Windows aplikacije i dalje ga čitaju na isti način. CopyToClipboard stoji uz PasteFromClipboard, suprotnu stranu iste razmene, na široj površini za međuspremnik i izvoz opisanoj na stranici proizvoda HotXLS Delphi Component