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
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:
// 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 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
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 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;
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
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');
// 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;
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); // 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;
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 Component