Teknik Makale

Delphi'de CF_HTML Pano Formatını Uygulamak

Bir Delphi grid'inden bir aralığı kopyalayıp Word'e yapıştırın, biçimlendirme genellikle kaybolur: düz metin, kalın başlık yok, kenarlık yok, dolgu yok. HotXLS bu boşluğu TXLSRange.CopyToClipboard ile kapatır; bu, panoya düz Unicode metnin yanına bir CF_HTML pano yükü -bayt-tam parça işaretçileriyle biçimlendirilmiş HTML için Windows formatı- koyar

Bu, bir CF_HTML yükünün gerçekte ne gerektirdiğine bakana kadar basit görünür. Format, parçanın daha büyük pano arabelleği içinde tam olarak nerede başlayıp bittiğini adlandıran kısa bir metin başlığına ihtiyaç duyar ve bu konumlar, HTML'in sonunda hangi çok baytlı kodlamada olursa olsun o kodlama üzerinden sayılan bayt konumlarıdır. Aritmetiği bir bayt bile yanlış yapın, hedef uygulama ya yanlış bir işaretleme dilimini kapar ya da vazgeçip düz metne döner ve iki başarısızlık da kodunuzdaki bir hata gibi görünmez -Word'ün Word olması gibi görünür

Bir Delphi grid'inden kopyala-yapıştır neden genellikle biçimlendirmesini kaybeder?

Çoğu Delphi kodunun başvurduğu varsayılan Windows pano çağrısı, CF_TEXT veya CF_UNICODETEXT ile SetClipboardData, yalnızca düz karakterler taşır; bu yüzden kaynak grid'de uygulanan herhangi bir stilin gidecek hiçbir yeri yoktur. Word, Outlook ve her Chromium tabanlı tarayıcı, yapıştırdığınızda daha zengin bir format arar: seçimin satır içi stiller, tablo yapısı ve bağlantılarla birlikte tam bir HTML temsili. Excel'in kendisi tam olarak bu numaraya dayanır -Excel'de bir aralığı kopyalayın ve pano sessizce aynı anda birkaç format alır, bunların arasında HTML de vardır; bu yüzden yapıştırdığınız uygulama ne olursa olsun anladığı en zengin olanı seçer. Yalnızca CF_UNICODETEXT yazan bir bileşen, bu daha zengin tüketicilerin her birine üzerinde çalışacak hiçbir şey vermez ve kullanıcının az önce kopyaladığı görsel zenginlik yapıştırmak için orada değildir

CF_HTML pano formatı tam olarak nedir?

CF_HTML, CF_TEXT gibi sabit bir sistem pano formatı değildir; ada göre RegisterClipboardFormat('HTML Format') ile istenen dinamik olarak kaydedilmiş bir formattır ve yükü, ardından bir HTML belgesi veya parçası gelen kısa bir ASCII başlığıdır. Başlık beş alan taşır -Version, StartHTML, EndHTML, StartFragment, EndFragment- burada Version her zaman 0.9'dur ve diğer dördü ASCII rakamları olarak yazılmış ondalık sayılardır. StartHTML ve EndHTML, alıcı uygulamanın bağlam için ayrıştırması gereken -yazı tipleri ve stiller dahil- tüm belgeyi sınırlarken, StartFragment ve EndFragment, imlece gerçekte inen daha dar dilimi sınırlar; geleneksel olarak işaretlemenin kendisinde <!--StartFragment--> ve <!--EndFragment--> yorumlarıyla işaretlenir ki sınırlar naif bir yeniden serileştirmeden sağ çıksın

Karakter sayısı değil bayt konumu: klasik CF_HTML tuzağı

CF_HTML'in dört sayısal başlık alanı, başlığın kendisinin tam ilk karakterinden sayılan, pano üzerinde oturan tam bayt dizisine olan bayt konumlarıdır -karakter sayısı değil, Unicode kod noktası değil ve parçaya veya <body> etiketine göreli konum hiç değil. Bu ayrım, elle yazılmış CF_HTML uygulamalarının sessizce yanlış gittiği yerdir: bir Delphi UnicodeString'inin Length'i UTF-16 kod birimlerini bildirir ki bu düz ASCII metin için bayt sayısına eşit olur; bu yüzden hata, İngilizce örnek verilerle yazılmış herhangi bir testten sorunsuzca geçer ve yalnızca kopyalanan bir hücre bir tire, bir para birimi sembolü veya aksanlı bir karakter tuttuğunda ortaya çıkar -bir euro işareti bir UTF-16 kod birimidir ama UTF-8'de üç bayttır ve bu noktadan sonra hesaplanan her konum, kodlamanın eklediği kadar ekstra baytla kayar. Bunu izleyen başarısızlık bir çökme değildir; alıcı uygulamanın başlığın işaret ettiği tam bayt aralığını kapması, bir etiketin ortasında başlayan veya biten bir işaretleme dilimi bulması ve ya çöp render etmesi ya da vazgeçip panoda onun yanında duran her ne düz metin varsa ona sessizce, kodunuzda nedenini açıklayacak hiçbir şey olmadan geri dönmesidir -tam olarak bu başarısızlığı üreten kodun şekli budur:

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

HotXLS başlığı nasıl bayt-doğru tutuyor?

HotXLS bu hata sınıfını yapısal olarak önler: TXLSRange.CopyToClipboard ve altındaki lxClipboard birimi, CF_HTML belgesini ve başlığını tamamen Delphi'nin bayt-dizesi türü olan AnsiString olarak oluşturur; bu yüzden Length ve Pos hesaplama boyunca her yerde zaten bayt konumları döndürür -bir Unicode karakter sayısının başlığa girmeden önce bir bayt sayısına dönüştürülmesi gereken ayrı bir adım yoktur, dolayısıyla unutulacak bir adım da yoktur

Bir CF_HTML başlığını elle oluşturursanız bilmeye değer ikinci, daha küçük bir numara daha var. Başlık iki kez yazılır: bir kez dört konumun her biri yerine geçen on sıfır rakamıyla, böylece kendi bayt uzunluğu ölçülebilir, ve bir kez daha gerçek konumlar yamalanmış olarak. Her gerçek konum aynı sabit on basamaklı genişliğe biçimlendirildiğinden, ikinci başlık, yer tutucu sürümle bayt bayt aynı uzunlukta çıkar ki bu tam olarak önceki ölçümün yeniden yazımdan sonra geçerli kalmasının nedenidir. Sabit genişliği atlayın, bir sayıyı düz bir IntToStr ile biçimlendirin, ve başlık iki geçiş arasında bir rakam kadar küçülebilir veya büyüyebilir; bu da onu izleyen her konumu sessizce geçersiz kılar:

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;

Düz metin yükü neden yine de yanında gitmek zorunda?

TXLSRange.CopyToClipboard, CF_HTML'i asla panoya tek başına koymaz; her zaman aynı çağrıda CF_UNICODETEXT de yazar, çünkü CF_HTML her Windows uygulamasının zaten aramayı bildiği sabit CF_* sabitlerinden biri değil, kayıtlı bir formattır -düz bir metin düzenleyicisi, eski bir grid veya 'HTML Format''ı hiç kontrol etmeyen herhangi bir şey onu hiç görmeyecektir ve kopyaladığınız aralık ya sekmeyle ayrılmış metin olarak gelir ya da hiç gelmez. O sekmeyle ayrılmış metin de kaba bir yaklaşım değildir: formül hücreleri, saklanan metin bunu düşürmüşse başına = yeniden eklenmiş formül dizeleri olarak kopyalanır ve bu, Excel'in kendi pano metninin davrandığı şekille eşleşir; sıradan hücreler FormattedText'lerini -görüntülendiği haliyle dizeyi- kopyalar, bu yüzden bir para birimi hücresi altta yatan 1234.56 değil $1.234,56 olarak kopyalanır- ve bir sekme, bir tırnak veya bir satır sonu içeren herhangi bir alan, gömülü tırnaklar ikiye katlanarak tırnak içine alınır; bu, CSV'nin kullandığı aynı kuraldır

SaveAsHTML, yalnızca pano durumu için eklenmiş ayrı bir render yolu değildir. CopyToClipboard, HotXLS'in CSV, TSV ve HTML dışa aktarımında tanımlanan tam olarak aynı HTML yazıcısını çağırır, ardından bu yazıcının ürettiği her neyse bağımsız bir dosya olarak kaydetmek yerine CF_HTML zarfına sarar; bu yüzden o HTML hakkında doğru olan her şey doğrudan panoya inene taşınır. Bir çalışma sayfası aralığını tek bir çağrıda her iki format olarak toplamak şöyle görünür:

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;

Yapıştırılan aralık yazı tiplerini, renklerini ve birleştirilmiş hücrelerini korur mu?

Evet, çünkü yükün HTML yarısı, çıplak bir veri dökümü değil, aralığın tam bir render'ıdır: yazı tipleri, dolgu renkleri, kenarlıklar, sayı formatları ve birleştirilmiş hücreler, HotXLS'in koşullu biçimlendirme ve zengin metin rehberinde ele alınan aynı stil makinesiyle, satır içi stiller ve tablo yapısı olarak gelir; çünkü bir hücrenin zengin metin çalışmaları ve koşullu biçimlendirme sonucu, ikisi de CopyToClipboard'ın okuduğu aynı render'ı besler. Yolculuktan sağ çıkmayan şey canlı formül davranışıdır: bir formül hücresinin düz metin biçimi formül dizesini taşır, bu yüzden elektronik tablo farkında bir yapıştırma hedefi prensipte onu yeniden hesaplayabilir, ama HTML biçimi yalnızca son hesaplanan sonucu taşır, çünkü HTML'in bir tarayıcı veya bir kelime işlemcinin değerlendireceği bir formül kavramı yoktur

Yapıştırmayı doğrulamak ve meşgul bir panoyu ele almak

İki alışkanlık, bir müşteriden önce çoğu pano sorununu yakalar. Önce Notepad'e yapıştırarak CF_UNICODETEXT yedeğinin makul, sekmeyle ayrılmış metin olduğunu doğrulayın, sonra aynı kopyayı Word'e veya bir tarayıcıya yapıştırarak biçimlendirilmiş sürümün göründüğünü doğrulayın -birinde doğru görünen ama diğerinde yanlış görünen bir yük genellikle parça işaretçilerinin yanlış yere indiği anlamına gelir. Ardından CopyToClipboard'ın döndürdüğü Boolean sonucu süslü değil anlamlı olarak ele alın: OpenClipboard, başka bir süreç panoyu açık tutarken başarısız olabilir; meşgul bir masaüstünde yeterince yaygındır ki kontrol edilmeyen bir çağrı sonunda nedenini açıklayacak hiçbir hata olmadan hiçbir şey yapıştırmaz; aşağıdaki yeniden deneme bunu tam olarak korur:

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;

Başlık bayt-doğru olduğunda ve düz metin yedeği içeriği hakkında dürüst olduğunda, formatın kendisi egzotik değildir -Internet Explorer onu ilk tanımladığından beri büyük ölçüde değişmeden var olmuştur ve her büyük Windows uygulaması onu hâlâ aynı şekilde okur. CopyToClipboard, HotXLS Bileşeni ürün sayfasında belgelenen daha geniş pano ve dışa aktarım yüzeyinde, aynı alışverişin okuma tarafı olan PasteFromClipboard'ın yanında yer alır