Artigo Técnico

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

Copie um intervalo de um grid Delphi e cole no Word, e a formatação geralmente desaparece: texto simples, sem cabeçalhos em negrito, sem bordas, 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 em bytes — na área de transferência ao lado de texto Unicode simples

Isso parece simples até você olhar o que um payload CF_HTML de fato exige. O formato precisa de um cabeçalho de texto curto nomeando 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 qualquer codificação multi-byte em que o HTML acabe. Erre a aritmética por apenas um byte, e a aplicação alvo ou pega a fatia errada de marcação, ou desiste e recai para texto simples, e nenhuma das duas falhas parece um bug no seu código — parece o Word sendo Word

Por que copiar e colar de um grid Delphi geralmente perde a formatação

A chamada padrão de área de transferência do Windows que a maioria do código Delphi usa, SetClipboardData com CF_TEXT ou CF_UNICODETEXT, só carrega caracteres simples, de modo que qualquer estilo aplicado no grid de origem não tem para onde ir. Word, Outlook e todo navegador baseado em Chromium procuram por um formato mais rico ao colar: uma representação HTML da seleção, completa com estilos inline, estrutura de tabela e links. O próprio Excel se apoia exatamente nesse truque — copie um intervalo no Excel e a área de transferência silenciosamente recebe vários formatos de uma vez, HTML entre eles, de modo que qualquer aplicação em que você cole escolhe a mais rica que entende. Um componente que só escreve CF_UNICODETEXT entrega a cada um desses consumidores mais ricos nada para trabalhar, e a riqueza visual que o usuário 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 de área de transferência de sistema fixo como CF_TEXT; é um formato registrado dinamicamente, solicitado pelo nome por meio de RegisterClipboardFormat('HTML Format'), e seu payload é um cabeçalho ASCII curto seguido de um documento ou fragmento HTML. O cabeçalho carrega 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 como a aplicação receptora deve analisá-lo para contexto, fontes e estilos incluídos, enquanto StartFragment e EndFragment delimitam a fatia mais estreita que de fato pousa no cursor, convencionalmente marcada na própria marcação com comentários <!--StartFragment--> e <!--EndFragment--> para que os limites sobrevivam a uma reserializiação ingênua

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

Os quatro campos numéricos de cabeçalho do CF_HTML são deslocamentos de byte na sequência exata de bytes sentada na área de transferência, contados a partir do primeiríssimo caractere do próprio cabeçalho — não contagens de caractere, não pontos de código Unicode, e não deslocamentos relativos ao fragmento ou à tag <body>. Essa distinção é onde implementações de CF_HTML feitas à mão silenciosamente dão errado: a propriedade Length de uma UnicodeString do Delphi reporta unidades de código UTF-16, que por acaso é igual à contagem de bytes para texto ASCII simples, de modo que o bug passa limpo por qualquer teste escrito com dados de amostra em inglês e só aparece quando uma célula copiada contém um travessão, um símbolo de moeda ou um caractere acentuado — um sinal de euro é uma unidade de código UTF-16, mas três bytes em UTF-8, e todo deslocamento calculado depois desse ponto desvia por quantos bytes extras a codificação adicionou. A falha que se segue não é uma quebra; é a aplicação receptora agarrando exatamente o intervalo de bytes para o qual o cabeçalho apontou, encontrando uma fatia de marcação que começa ou termina no meio de uma tag, e ou renderizando lixo ou desistindo e recaindo para qualquer texto simples que esteja ao lado dela na área de transferência, silenciosamente, sem nada em seu código para explicar o porquê — eis a forma de código que produz exatamente essa falha:

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

Como o HotXLS mantém o cabeçalho preciso em bytes

O HotXLS evita essa classe de bug estruturalmente: TXLSRange.CopyToClipboard e a unit lxClipboard por baixo dela constroem o documento CF_HTML e seu cabeçalho inteiramente como AnsiString, o tipo de string de byte do Delphi, de modo que Length e Pos já retornam posições de byte em todo o cálculo — não há etapa separada, e portanto nenhuma etapa para esquecer, onde uma contagem de caractere Unicode precisaria ser convertida em uma contagem de byte antes de entrar no cabeçalho

Há um segundo truque, menor, que vale a pena conhecer se você algum dia construir um cabeçalho CF_HTML à mão. O cabeçalho é escrito duas vezes: uma vez com dez dígitos zero substituindo cada um dos quatro deslocamentos, para que seu próprio comprimento em bytes possa ser medido, e mais uma vez com os deslocamentos reais preenchidos. Como todo deslocamento real é formatado para essa mesma largura fixa de dez dígitos, o segundo cabeçalho sai byte a byte do mesmo comprimento que a versão placeholder, que é exatamente por que a medição anterior permanece válida depois da reescrita. Pule a largura fixa, formate um número com um simples IntToStr em vez disso, e o cabeçalho pode encolher ou crescer por um dígito entre as duas passadas, invalidando silenciosamente todo deslocamento que o segue:

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;

Por que o payload de texto simples ainda precisa acompanhar

TXLSRange.CopyToClipboard nunca coloca CF_HTML na área de transferência sozinho; sempre escreve CF_UNICODETEXT na mesma chamada, porque CF_HTML é um formato registrado, e não uma das constantes fixas CF_* que toda aplicação Windows já sabe procurar — um editor de texto simples, um grid legado, ou qualquer coisa que nunca verificou por 'HTML Format' não vai vê-lo de forma alguma, e o intervalo que você copiou ou chega como texto delimitado por tabulação ou não chega. Esse texto delimitado por tabulação não é uma aproximação grosseira, também: células de fórmula copiam como sua string de fórmula com um = inicial restaurado se o texto armazenado o descartou, correspondendo a como o próprio texto de área de transferência do Excel se comporta, células comuns copiam seu FormattedText — a string como exibida, de modo que uma célula de moeda copia como R$ 1.234,56, não o valor subjacente 1234.56 — e qualquer campo contendo uma tabulação, uma aspa ou uma quebra de linha é colocado entre aspas com aspas embutidas duplicadas, a mesma convenção que o CSV usa

SaveAsHTML não é um caminho de renderização separado parafusado só 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, depois envolve o que quer que esse escritor produza no envelope CF_HTML em vez de salvá-lo como um arquivo autônomo, de modo que qualquer coisa verdadeira sobre esse HTML passa direto para o que pousa na área de transferência. Reunir um intervalo de planilha como os dois formatos em uma chamada fica assim:

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;

O intervalo colado mantém suas fontes, cores e células mescladas?

Sim, porque a metade HTML do payload é uma renderização completa do intervalo, não um simples despejo de dados: fontes, cores de preenchimento, bordas, formatos de número e células mescladas todos passam como estilos inline e estrutura de tabela, a mesma maquinaria de estilo coberta em o guia do HotXLS sobre formatação condicional e rich text, já que tanto as sequências de rich text de uma célula quanto 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 carrega a string da fórmula, de modo que um alvo de colagem com conhecimento de planilha poderia em princípio recalculá-la, mas a forma HTML só carrega o último resultado calculado, porque HTML não tem noção de fórmula para um navegador ou processador de texto avaliar

Verificando a colagem, e lidando com uma área de transferência ocupada

Dois hábitos capturam a maioria dos problemas de área de transferência antes de um cliente o fazer. Cole no Bloco de Notas primeiro para confirmar que o fallback CF_UNICODETEXT é texto delimitado por tabulação são; depois cole a mesma cópia no Word ou em um navegador para confirmar que a versão estilizada aparece — um payload que parece certo em um e errado no outro geralmente significa que os marcadores de fragmento pousaram no lugar errado. Depois trate o resultado booleano que CopyToClipboard retorna como significativo, não decorativo: OpenClipboard pode falhar quando outro processo está segurando a área de transferência aberta, comum o suficiente em uma área de trabalho ocupada de modo que uma chamada não verificada eventualmente cola nada sem nenhum erro para explicar o porquê, que é contra o que a nova tentativa 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);   // 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;

O formato em si não é exótico, uma vez que o cabeçalho é preciso em bytes e o fallback de texto simples é honesto sobre o que contém — ele existe praticamente inalterado desde que o Internet Explorer o definiu pela primeira vez, e toda aplicação Windows importante ainda o lê da mesma forma. CopyToClipboard fica ao lado 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 de produto do Componente HotXLS