Artigo Técnico

Implementar o Formato de Área de Transferência CF_HTML em Delphi

Copie um intervalo de uma grelha Delphi e cole-o no Word, e a formatação normalmente desaparece: texto simples, sem cabeçalhos a negrito, sem contornos, sem preenchimentos. O HotXLS fecha essa lacuna com TXLSRange.CopyToClipboard, que coloca um payload de área de transferência CF_HTML — o formato do Windows para HTML estilizado com marcadores de fragmento exatos ao byte — na área de transferência a par de texto Unicode simples

Isso parece simples até se olhar para o que um payload CF_HTML efetivamente exige. O formato precisa de um pequeno cabeçalho de texto que nomeie exatamente onde o fragmento começa e termina dentro do buffer maior da área de transferência, e essas posições são deslocamentos de byte, contados através de seja qual for a codificação multi-byte em que o HTML acabe. Errar a aritmética por um único byte que seja e a aplicação alvo ou agarra a fatia errada de marcação ou desiste e recua para texto simples, e nenhuma das falhas parece um bug no seu código — parece o Word a ser o Word

Porque perde normalmente a cópia-colagem de uma grelha Delphi a sua formatação?

A chamada de área de transferência predefinida do Windows a que a maioria do código Delphi recorre, SetClipboardData com CF_TEXT ou CF_UNICODETEXT, só alguma vez transporta caracteres simples, pelo que qualquer estilo aplicado na grelha de origem não tem para onde ir. O Word, o Outlook, e todos os navegadores baseados em Chromium procuram um formato mais rico ao colar: uma representação HTML da seleção, completa com estilos inline, estrutura de tabela, e ligações. O próprio Excel apoia-se exatamente nesse truque — copie um intervalo no Excel e a área de transferência recebe discretamente vários formatos ao mesmo tempo, o HTML entre eles, pelo que qualquer aplicação em que se cole escolhe o mais rico que entenda. Um componente que só alguma vez escreve CF_UNICODETEXT entrega a cada um desses consumidores mais ricos nada com que trabalhar, e a riqueza visual que o utilizador acabou de copiar simplesmente não está lá para colar

O que é exatamente o formato de área de transferência CF_HTML?

CF_HTML não é um formato fixo do sistema de área de transferência como CF_TEXT; é um registado dinamicamente, pedido pelo nome através de RegisterClipboardFormat('HTML Format'), e o seu payload é um pequeno cabeçalho ASCII seguido de um documento ou fragmento HTML. O cabeçalho transporta cinco campos — Version, StartHTML, EndHTML, StartFragment, EndFragment — onde Version é sempre 0.9 e os outros quatro são números decimais escritos como dígitos ASCII. StartHTML e EndHTML delimitam todo o documento tal como a aplicação recetora o deve analisar para contexto, incluindo tipos de letra e estilos, enquanto StartFragment e EndFragment delimitam a fatia mais estreita que efetivamente cai no cursor, convencionalmente marcada na própria marcação com comentários <!--StartFragment--> e <!--EndFragment-->, para que os limites sobrevivam a uma reserialização ingénua

Diagrama de um payload CF_HTML da área de transferência construído pelo HotXLS em Delphi, mostrando o cabeçalho ASCII de cinco campos e o documento UTF-8 com os marcadores de comentário StartFragment e EndFragment
O envelope CF_HTML é um curto cabeçalho ASCII à frente de um documento UTF-8, com a fatia colada marcada pelos comentários StartFragment e EndFragment

Deslocamentos de byte, não contagens de caracteres: a armadilha clássica do CF_HTML

Os quatro campos numéricos do cabeçalho CF_HTML são deslocamentos de byte na sequência exata de bytes pousada na área de transferência, contados a partir do próprio primeiro caractere do cabeçalho — não contagens de caracteres, não pontos de código Unicode, e não deslocamentos relativos ao fragmento ou à etiqueta <body>. Essa distinção é onde implementações de CF_HTML feitas à mão falham discretamente: o Length de um UnicodeString Delphi reporta unidades de código UTF-16, que calham de ser iguais à contagem de bytes para texto ASCII simples, pelo que o bug passa limpo por qualquer teste escrito com dados de amostra em inglês e só aparece assim que uma célula copiada contém um travessão longo, um símbolo de moeda, ou um caractere acentuado — um símbolo de euro é uma unidade de código UTF-16 mas três bytes em UTF-8, e cada deslocamento calculado depois desse ponto desvia-se por quantos bytes extra a codificação acrescentou. A falha que se segue não é um crash; é a aplicação recetora a agarrar o intervalo de bytes exato para onde o cabeçalho apontou, a encontrar uma fatia de marcação que começa ou termina a meio de uma etiqueta, e ou a renderizar lixo ou a desistir e recuar para o que quer que texto simples esteja ao lado na área de transferência, silenciosamente, sem nada no seu código a explicar porquê — eis a forma de código que produz exatamente essa falha:

// Frágil: Length() numa UnicodeString conta unidades de código UTF-16, não 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);
  // Um símbolo de moeda, um travessão (em dash) ou qualquer caráter acentuado colocado
  // antes deste ponto custa um caractere aqui, mas dois ou três bytes
  // uma vez que o documento está codificado em UTF-8, o StartFragmentOfs agora aponta
  // aquém de onde o fragmento começa realmente na área de transferência real
end;

Como mantém o HotXLS o cabeçalho exato ao byte

O HotXLS evita esta classe de bug estruturalmente: TXLSRange.CopyToClipboard e a unidade lxClipboard por baixo dela constroem o documento CF_HTML e o seu cabeçalho inteiramente como AnsiString, o tipo de cadeia de bytes do Delphi, pelo que Length e Pos já devolvem posições de byte em toda a parte do cálculo — não há um passo separado, e portanto nenhum passo a esquecer, onde uma contagem de caracteres Unicode precisasse de ser convertida numa contagem de bytes antes de entrar no cabeçalho

Diagrama contrastando contagens de unidades de código UTF-16 com desvios de bytes UTF-8 num cabeçalho CF_HTML Delphi, em que caracteres acentuados e um sinal de euro deslocam as fronteiras dos fragmentos
Um único caractere multibyte desloca cada desvio de byte calculado depois dele, pelo que o HotXLS mede o cabeçalho inteiro em bytes AnsiString em vez de unidades de código

Há um segundo truque, mais pequeno, que vale a pena conhecer caso alguma vez se construa um cabeçalho CF_HTML à mão. O cabeçalho é escrito duas vezes: uma com dez dígitos zero a substituir cada um dos quatro deslocamentos, para que o seu próprio comprimento em bytes possa ser medido, e outra com os deslocamentos reais colocados. Porque cada deslocamento real é formatado para essa mesma largura fixa de dez dígitos, o segundo cabeçalho sai com exatamente o mesmo comprimento, byte a byte, que a versão de marcador de posição, o que é exatamente a razão pela qual a medição anterior permanece válida após a reescrita. Ignore a largura fixa, formate um número com um simples IntToStr em vez disso, e o cabeçalho pode encolher ou crescer um dígito entre as duas passagens, invalidando discretamente cada deslocamento que se segue

const
  Placeholder = '0000000000';   // 10 dígitos ASCII: largura fixa na entrada, largura fixa na saída
var
  Header: AnsiString;           // AnsiString.Length é uma contagem de bytes, não uma contagem de caracteres
  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);   // seguro de medir uma vez, logo no início
  // ...calcule os deslocamentos reais contra o documento AnsiString...
  // depois reconstrua Header com os números reais formatados com a mesma
  // largura de 10 dígitos, de modo que o seu comprimento de bytes -- e, portanto, StartHtmlOfs --
  // nunca se move entre a passagem de marcador de posição e a final
end;

Porque tem o payload de texto simples de continuar a acompanhar

TXLSRange.CopyToClipboard nunca coloca CF_HTML sozinho na área de transferência; escreve sempre CF_UNICODETEXT na mesma chamada, porque CF_HTML é um formato registado em vez de uma das constantes fixas CF_* que todas as aplicações Windows já sabem procurar — um editor de texto simples, uma grelha legada, ou qualquer coisa que nunca tenha verificado 'HTML Format' não o verá de todo, e o intervalo que copiou ou chega como texto delimitado por tabulações ou não chega. Esse texto delimitado por tabulações também não é uma aproximação grosseira: as células de fórmula copiam-se como a sua cadeia de fórmula com um = inicial restaurado se o texto armazenado o tiver perdido, correspondendo ao comportamento do próprio texto de área de transferência do Excel, as células comuns copiam o seu FormattedText — a cadeia tal como apresentada, pelo que uma célula de moeda copia como $1.234,56, não o valor subjacente 1234.56 — e qualquer campo que contenha uma tabulação, uma aspa, ou uma quebra de linha é colocado entre aspas com as aspas embutidas duplicadas, a mesma convenção que o CSV usa

Diagrama de CopyToClipboard do HotXLS a escrever CF_HTML e CF_UNICODETEXT na área de transferência do Windows, para que o Word e os navegadores colem tabelas formatadas enquanto editores simples recebem texto delimitado por tabulações
CopyToClipboard escreve sempre uma metade em texto simples ao lado do HTML, para que todos os alvos, do Word ao Notepad, recebam algo honesto

SaveAsHTML não é um caminho de renderização separado, aparafusado apenas para o caso da área de transferência. CopyToClipboard chama exatamente o mesmo escritor HTML descrito em a exportação CSV, TSV, e HTML do HotXLS, e depois envolve o que quer que esse escritor produza no envelope CF_HTML em vez de o guardar como um ficheiro autónomo, pelo que tudo o que é verdade sobre esse HTML transporta-se diretamente para o que cai na área de transferência. Reunir um intervalo de folha de cálculo em ambos os formatos numa chamada tem este aspeto:

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-report.xlsx');
    // Os intervalos clássicos TXLSWorkbook expõem o mesmo método que
    // 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;

Mantém o intervalo colado os seus tipos de letra, cores, e células combinadas?

Sim, porque a metade HTML do payload é uma renderização completa do intervalo, não um simples despejo de dados: tipos de letra, cores de preenchimento, contornos, formatos numéricos, e células combinadas passam todos como estilos inline e estrutura de tabela, o mesmo mecanismo de estilo abordado em o guia do HotXLS sobre formatação condicional e texto formatado, uma vez que tanto os trechos de texto formatado de uma célula como o resultado da formatação condicional alimentam a mesma renderização de onde CopyToClipboard lê. O que não sobrevive à viagem é o comportamento de fórmula viva: a forma de texto simples de uma célula de fórmula transporta a cadeia de fórmula, pelo que um alvo de colagem consciente de folhas de cálculo poderia em princípio recalculá-la, mas a forma HTML só alguma vez transporta o último resultado calculado, porque o HTML não tem qualquer conceito de fórmula para um navegador ou processador de texto avaliar

Verificar a colagem, e tratar uma área de transferência ocupada

Dois hábitos apanham a maioria dos problemas de área de transferência antes de um cliente o fazer. Cole primeiro no Bloco de Notas para confirmar que o recuo CF_UNICODETEXT é texto delimitado por tabulações são; depois cole a mesma cópia no Word ou num navegador para confirmar que a versão estilizada aparece — um payload que parece correto num e errado no outro normalmente significa que os marcadores de fragmento caíram no sítio errado. Depois trate o resultado booleano que CopyToClipboard devolve como significativo, não decorativo: OpenClipboard pode falhar quando outro processo está a manter a área de transferência aberta, suficientemente comum num ambiente de trabalho ocupado para que uma chamada não verificada acabe por não colar nada sem qualquer erro a explicar porquê, que é contra o que a repetição abaixo se protege:

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);   // dê um momento a qualquer aplicação que esteja a manter a área de transferência
  end;
  if not Result then
    raise Exception.Create('Could not take ownership of the clipboard');
end;

O próprio formato não é exótico assim que o cabeçalho é exato ao byte e o recuo de texto simples é honesto quanto ao que contém — existe praticamente inalterado desde que o Internet Explorer o definiu pela primeira vez, e todas as principais aplicações Windows continuam a lê-lo da mesma forma. CopyToClipboard situa-se a par de PasteFromClipboard, o lado de leitura da mesma troca, na superfície mais ampla de área de transferência e exportação documentada na página do produto componente HotXLS