Salin sebuah rentang dari sebuah grid Delphi dan tempelkan ke Word, dan formatnya biasanya lenyap: teks polos, tanpa header tebal, tanpa border, tanpa fill. HotXLS menutup celah itu dengan TXLSRange.CopyToClipboard, yang menempatkan sebuah payload clipboard CF_HTML — format Windows untuk HTML bergaya dengan marker fragmen presisi-byte — pada clipboard di sebelah teks Unicode polos
Itu terdengar sederhana sampai Anda melihat apa yang sebenarnya dibutuhkan sebuah payload CF_HTML. Format itu membutuhkan sebuah header teks pendek yang menyebutkan persis di mana fragmen dimulai dan berakhir di dalam buffer clipboard yang lebih besar, dan posisi-posisi itu adalah offset byte, dihitung lewat encoding multi-byte apa pun yang akhirnya digunakan HTML tersebut. Salahkan aritmetikanya sekalipun hanya satu byte dan aplikasi target akan mengambil potongan markup yang salah atau menyerah dan jatuh kembali ke teks polos, dan tidak satu pun kegagalan itu terlihat seperti bug di kode Anda — terlihat seperti Word sedang berperilaku seperti Word
Mengapa copy-paste dari sebuah grid Delphi biasanya kehilangan formatnya
Pemanggilan clipboard Windows default yang biasa dijangkau kebanyakan kode Delphi, SetClipboardData dengan CF_TEXT atau CF_UNICODETEXT, hanya pernah membawa karakter polos, sehingga styling apa pun yang diterapkan di grid sumber tidak punya tempat untuk pergi. Word, Outlook, dan setiap browser berbasis Chromium mencari sebuah format yang lebih kaya ketika Anda menempel: sebuah representasi HTML dari seleksi tersebut, lengkap dengan inline style, struktur tabel, dan link. Excel sendiri mengandalkan persis trik ini — salin sebuah rentang di Excel dan clipboard diam-diam menerima beberapa format sekaligus, HTML di antaranya, sehingga aplikasi mana pun yang Anda tempeli memilih yang paling kaya yang dipahaminya. Sebuah komponen yang hanya pernah menulis CF_UNICODETEXT menyerahkan kepada setiap konsumen yang lebih kaya itu tidak ada apa pun untuk dikerjakan, dan kekayaan visual yang baru saja disalin pengguna sekadar tidak ada untuk ditempel
Apa sebenarnya format clipboard CF_HTML itu?
CF_HTML bukan sebuah format clipboard sistem tetap seperti CF_TEXT; ia terdaftar secara dinamis, diminta lewat nama melalui RegisterClipboardFormat('HTML Format'), dan payload-nya adalah sebuah header ASCII pendek diikuti sebuah dokumen atau fragmen HTML. Header itu membawa lima field — Version, StartHTML, EndHTML, StartFragment, EndFragment — di mana Version selalu 0.9 dan keempat lainnya adalah angka desimal yang ditulis sebagai digit ASCII. StartHTML dan EndHTML membatasi seluruh dokumen sebagaimana aplikasi penerima seharusnya mem-parse-nya untuk konteks, font dan style termasuk, sementara StartFragment dan EndFragment membatasi potongan yang lebih sempit yang benar-benar mendarat di kursor, secara konvensional ditandai dalam markup itu sendiri dengan komentar <!--StartFragment--> dan <!--EndFragment--> sehingga batasnya bertahan dari serialisasi ulang naif
Offset byte, bukan hitungan karakter: jebakan klasik CF_HTML
Keempat field numerik header CF_HTML adalah offset byte ke dalam urutan byte persis yang duduk di clipboard, dihitung dari karakter paling pertama header itu sendiri — bukan hitungan karakter, bukan code point Unicode, dan bukan offset relatif terhadap fragmen atau tag <body>. Perbedaan itulah tempat implementasi CF_HTML buatan tangan diam-diam menjadi salah: Length milik sebuah UnicodeString Delphi melaporkan unit kode UTF-16, yang kebetulan sama dengan hitungan byte untuk teks ASCII polos, sehingga bug itu lolos bersih lewat pengujian mana pun yang ditulis dengan data sampel berbahasa Inggris dan baru terlihat begitu sebuah sel yang disalin memuat sebuah em dash, sebuah simbol mata uang, atau sebuah karakter beraksen — sebuah tanda euro adalah satu unit kode UTF-16 tetapi tiga byte dalam UTF-8, dan setiap offset yang dihitung setelah titik itu melenceng sebanyak apa pun byte ekstra yang ditambahkan encoding tersebut. Kegagalan yang mengikuti bukan sebuah crash; itu aplikasi penerima menyita persis rentang byte yang ditunjuk header, menemukan sebuah potongan markup yang dimulai atau berakhir di tengah tag, dan baik merender sampah atau menyerah dan jatuh kembali ke teks polos apa pun yang duduk di sebelahnya di clipboard, secara diam-diam, tanpa apa pun dalam kode Anda yang menjelaskan mengapa — berikut bentuk kode yang menghasilkan persis kegagalan itu:
// 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;
Bagaimana HotXLS menjaga header tetap akurat-byte
HotXLS menghindari kelas bug ini secara struktural: TXLSRange.CopyToClipboard dan unit lxClipboard di baliknya membangun dokumen CF_HTML beserta headernya sepenuhnya sebagai AnsiString, tipe byte-string milik Delphi, sehingga Length dan Pos sudah mengembalikan posisi byte di mana pun dalam perhitungan tersebut — tidak ada langkah terpisah, dan karenanya tidak ada langkah yang bisa terlupa, di mana sebuah hitungan karakter Unicode perlu dikonversi menjadi sebuah hitungan byte sebelum masuk ke header
Ada sebuah trik kedua yang lebih kecil, layak diketahui jika Anda pernah membangun sebuah header CF_HTML dengan tangan. Header itu ditulis dua kali: sekali dengan sepuluh digit nol menggantikan masing-masing dari empat offset, sehingga panjang byte-nya sendiri bisa diukur, dan sekali lagi dengan offset sesungguhnya ditambal masuk. Karena setiap offset sesungguhnya diformat ke lebar sepuluh-digit tetap yang sama itu, header kedua keluar persis sama panjang byte-nya dengan versi placeholder, dan itulah persis mengapa pengukuran sebelumnya tetap valid setelah penulisan ulang. Lewati lebar tetap itu, format sebuah angka dengan IntToStr polos sebagai gantinya, dan header bisa menyusut atau tumbuh satu digit di antara kedua langkah, diam-diam membatalkan setiap offset yang mengikutinya:
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;
Mengapa payload teks polos tetap harus ikut serta
TXLSRange.CopyToClipboard tidak pernah menempatkan CF_HTML di clipboard sendirian; ia selalu menulis CF_UNICODETEXT dalam pemanggilan yang sama, karena CF_HTML adalah format terdaftar alih-alih salah satu konstanta CF_* tetap yang sudah diketahui setiap aplikasi Windows untuk dicari — sebuah editor teks polos, sebuah grid lawas, atau apa pun yang tidak pernah memeriksa 'HTML Format' tidak akan melihatnya sama sekali, dan rentang yang Anda salin baik tiba sebagai teks berpemisah-tab atau tidak tiba sama sekali. Teks berpemisah-tab itu juga bukan sebuah aproksimasi kasar: sel formula disalin sebagai string formulanya dengan sebuah = di depan yang dipulihkan jika teks tersimpan menghilangkannya, cocok dengan bagaimana teks clipboard Excel sendiri berperilaku, sel biasa menyalin FormattedText-nya — string sebagaimana ditampilkan, sehingga sebuah sel mata uang disalin sebagai $1,234.56, bukan 1234.56 yang mendasarinya — dan field apa pun yang berisi sebuah tab, sebuah tanda kutip, atau sebuah pemisah baris dikutip dengan tanda kutip tersemat digandakan, konvensi yang sama yang digunakan CSV
SaveAsHTML bukan sebuah jalur rendering terpisah yang ditempelkan hanya untuk kasus clipboard. CopyToClipboard memanggil penulis HTML yang persis sama yang dijelaskan di ekspor CSV, TSV, dan HTML milik HotXLS, lalu membungkus apa pun yang dihasilkan penulis itu dalam amplop CF_HTML alih-alih menyimpannya sebagai sebuah file mandiri, sehingga apa pun yang benar tentang HTML itu berlanjut langsung ke apa yang mendarat di clipboard. Menarik sebuah rentang worksheet bersama sebagai kedua format dalam satu pemanggilan terlihat seperti ini:
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;
Apakah rentang yang ditempel mempertahankan font, warna, dan sel gabungan-nya?
Ya, karena separuh HTML dari payload adalah sebuah rendering penuh dari rentang tersebut, bukan sebuah dump data telanjang: font, warna fill, border, format angka, dan sel gabungan semuanya ikut sebagai inline style dan struktur tabel, mesin styling yang sama yang dibahas di panduan HotXLS untuk conditional formatting dan rich text, karena run rich text sebuah sel dan hasil conditional formatting-nya sama-sama memberi makan rendering yang sama yang dibaca CopyToClipboard. Yang tidak bertahan dalam perjalanan adalah perilaku formula-hidup: bentuk teks-polos sebuah sel formula membawa string formulanya, sehingga sebuah target paste yang sadar-spreadsheet secara prinsip bisa menghitung ulangnya, tetapi bentuk HTML hanya pernah membawa hasil terhitung terakhirnya, karena HTML tidak memiliki konsep sebuah formula untuk dievaluasi sebuah browser atau word processor
Memverifikasi paste, dan menangani clipboard yang sibuk
Dua kebiasaan menangkap sebagian besar masalah clipboard sebelum seorang pelanggan melakukannya. Tempel ke Notepad lebih dulu untuk mengonfirmasi fallback CF_UNICODETEXT adalah teks berpemisah-tab yang masuk akal, lalu tempel salinan yang sama ke Word atau sebuah browser untuk mengonfirmasi versi bergaya-nya muncul — sebuah payload yang terlihat benar di satu dan salah di lainnya biasanya berarti marker fragmen mendarat di tempat yang salah. Kemudian perlakukan hasil Boolean yang dikembalikan CopyToClipboard sebagai bermakna, bukan hiasan: OpenClipboard bisa gagal ketika proses lain sedang memegang clipboard terbuka, cukup umum pada sebuah desktop yang sibuk sehingga satu pemanggilan yang tidak diperiksa akhirnya menempel tanpa apa pun tanpa error yang menjelaskan mengapa, yang dijaga retry berikut ini:
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;
Format itu sendiri tidak eksotis begitu headernya akurat-byte dan fallback teks-polos-nya jujur tentang apa yang dikandungnya — ia sudah ada sebagian besar tanpa berubah sejak Internet Explorer pertama kali mendefinisikannya, dan setiap aplikasi Windows besar masih membacanya dengan cara yang sama. CopyToClipboard duduk berdampingan dengan PasteFromClipboard, sisi baca dari pertukaran yang sama, dalam permukaan clipboard dan ekspor yang lebih luas yang didokumentasikan pada halaman produk HotXLS Component