Artigo Técnico

Slicers de tabela do Excel em Delphi: round-trip com HotXLS

A HotXLS cria slicers de tabela do Excel de Delphi e C++Builder com uma chamada — Worksheet.Slicers.Add(Table, ColumnName) — e escreve as oito localizações de pacote que um slicer precisa: as parts de slicer e slicerCache, os dois overrides de content type, os relacionamentos de worksheet e workbook com os blocos extLst correspondentes, e a âncora de drawing

Um slicer parece um controle que você solta numa planilha. No arquivo não é nada disso. É um registro distribuído espalhado por duas parts novas, dois grafos de relacionamento, três blocos de extensão e uma âncora de drawing, e o conjunto inteiro é validado como unidade quando o Excel abre o pacote. Erre uma peça e você não ganha um slicer degradado; ganha o diálogo de reparo, seguido de uma pasta de trabalho com o slicer removido silenciosamente. Esse modo de falha é o que torna slicers irritantes de emitir na mão, e é por que a engenharia interessante aqui é contabilidade em vez de renderização

Por que um slicer toca oito lugares no pacote?

Porque um slicer são três conceitos separados que o OPC força a três casas separadas: o estado do filtro (escopo da pasta de trabalho), a view (escopo da worksheet) e a shape (escopo do drawing). A HotXLS escreve exatamente oito pontos de aterrissagem por slicer de tabela, e cada um deles é load-bearing

  • xl/slicers/slicerN.xml — uma part por worksheet, segurando toda view <slicer> daquela planilha no namespace http://schemas.microsoft.com/office/spreadsheetml/2009/9/main
  • xl/slicerCaches/slicerCacheN.xml — nível de pasta de trabalho, uma por coluna de tabela fatiada, carregando o binding de verdade
  • um <Override> em [Content_Types].xml para cada uma dessas duas parts, usando application/vnd.ms-excel.slicer+xml e application/vnd.ms-excel.slicerCache+xml (a ECMA-376 Parte 2 exige um content type para toda part, e o Excel recusa o pacote sem eles)
  • um relacionamento de worksheet do tipo .../office/2007/relationships/slicer, mais uma extensão <x14:slicerList> dentro do extLst da worksheet apontando para esse rid
  • um relacionamento de workbook do tipo .../office/2007/relationships/slicerCache, mais uma extensão <x15:slicerCaches> dentro do extLst do workbook listando os mesmos rids
  • uma âncora mc:AlternateContent no drawing da planilha que dá ao slicer posição e tamanho
  • um bloco <x15:tableSlicerCache> dentro da part de cache que o liga a um id de tabela e a um id de coluna de tabela

Os blocos de extensão usam o mecanismo padrão extLst da ECMA-376 Parte 1 §18.2.10: cada <ext> carrega um atributo uri que identifica a feature, e leitores que não reconhecem o GUID pulam o bloco. A HotXLS discrimina slicers de tabela de slicers de pivô por esse GUID sozinho — {3A4CF648-6AED-40f4-86FF-DC5316D8AED3} para o slicerList de tabela, {A8765BA9-456A-4dab-B4F3-ACF838C121DE} para o de pivô, e correspondentemente {46BE6895-7355-4a93-B00E-2C351335B9C9} e {BBE1A952-AA13-448e-AADC-164F8A28A991} para as duas extensões slicerCaches do workbook. O binding de tabela em si vive sob {2F2917AC-EB37-4324-AD4E-5DD8C200BD13}. Essas constantes são expostas na unit como XlsxExtUriSlicerListX15, XlsxExtUriWbSlicerCachesX15 e XlsxExtUriTableSlicerCache, então nada no caminho de escrita carrega uma string mágica

As oito localizações de pacote que a HotXLS escreve para um slicer de tabela criado com Worksheet.Slicers.Add: as parts de slicer e slicerCache, dois overrides de content type, relacionamentos de worksheet e workbook, os blocos extLst x14:slicerList e x15:slicerCaches, e a âncora de drawing, cada um load-bearing quando o Excel valida o pacote
Uma chamada de Worksheet.Slicers.Add se espalha por oito pontos de aterrissagem no pacote: duas parts, dois overrides de content type, dois relacionamentos, dois blocos extLst e uma âncora de drawing, cada um validado como unidade quando o Excel abre o arquivo

Quais parts podem repetir o replay verbatim, e quais precisam ser reconstruídas?

A HotXLS responde com uma regra única: uma part pode reproduzir os bytes originais só se nada que ela referencia puder ser renumerado durante o save. Caches de slicer de tabela falham nesse teste, então a HotXLS os reconstrói a partir do modelo em todo save, incondicionalmente — mesmo quando o usuário nunca tocou no slicer. A razão é tableId: o elemento <x15:tableSlicerCache> identifica a tabela pelo atributo id de <table>, e a HotXLS renumera ids de tabela globalmente no workbook quando escreve. Uma implementação inicial guardava a part de cache como bytes crus no open e os empurrava de volta no save, que é a estratégia certa para a maioria das parts opacas. Ela produzia arquivos que abriam limpos no caso comum e disparavam um reparo no caso interessante: adicione uma tabela numa planilha anterior, salve, e o cache agora aponta para um id de tabela que pertence a outra coisa, ou a nada. O bug é invisível num teste de regressão simples de abrir-e-salvar porque os ids por acaso não se movem

Caches em forma de pivô tomam a rota oposta. O payload deles é <pivotTables> mais um bloco <x14:data> descrevendo itens cacheados, estado de sort e índices de item — uma estrutura sem renomeação do lado da HotXLS, e cara de modelar fielmente. Esses são mantidos como bytes crus e reproduzidos exatamente, com TXLSXSlicerCache.IsPivot e IsTableCache distinguindo os dois. As views de slicer seguem uma terceira política, de dirty-tracking: um elemento <slicer> intocado e a âncora de drawing dele se reproduzem verbatim, e no momento em que qualquer view da planilha é modificada, a part inteira da worksheet é regenerada a partir do modelo. Essa é a forma geral do julgamento — replay verbatim onde referências são estáveis, rebuild do modelo onde o writer reassina identidade, dirty tracking onde os dois são aceitáveis — e ela vale muito além de slicers, como as notas de round-trip sem perdas sobre themes, blocos extLst e calcChain detalham com mais profundidade

Política de replay por part de slicer na HotXLS: caches de slicer de tabela são sempre reconstruídos do modelo porque valores de tableId são renumerados globalmente no workbook no save, caches de pivô reproduzem os bytes crus já que pivotTables e x14:data nunca são renumerados, e views de slicer têm dirty-tracking, reproduzindo verbatim até qualquer view da planilha mudar
Política de replay por part: caches de slicer de tabela sempre reconstroem porque ids de tabela renumeram globalmente no workbook, caches de pivô reproduzem bytes crus já que nada neles é renumerado, e views têm dirty-tracking para que um save intocado fique byte-limpo

Criando um slicer de tabela em Delphi

TXLSXSlicers.Add é a API inteira de criação. Dado um TXLSXTable e um nome de coluna, ela materializa os ids de coluna da tabela, encontra ou cria o TXLSXSlicerCache de apoio, reserva o nome definido correspondente, e ancora uma view à direita da tabela. O overload de dois argumentos escolhe o posicionamento; o de seis argumentos recebe uma caixa inclusiva explícita baseada em 1

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Slicer: TXLSXSlicer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('sales.xlsx');
    Sheet := Book.Sheets[0];

    // Posicionamento padrão: duas colunas de folga da tabela,
    // 13 linhas de altura na altura de linha padrão de 19pt
    Slicer := Sheet.Slicers.Add(Sheet.Tables[0], 'Region');
    Slicer.Caption   := 'Sales region';
    Slicer.StyleName := 'SlicerStyleLight1';
    Slicer.ColumnCount := 2;

    Book.SaveAs('sales-sliced.xlsx');
  finally
    Book.Free;
  end;
end;

Duas regras de nomenclatura valem conhecer porque são convenções do Excel, não invenções da HotXLS. O cache é nomeado Slicer_<column> com caracteres não identificadores dobrados em underscores, e colisões ganham um sufixo numérico — Slicer_Region, depois Slicer_Region1, e assim por diante. E todo nome de cache também é reservado como nome definido do workbook cuja fórmula é #N/A, que é o que TXLSXSlicers.ReserveCacheDefinedName faz. O Excel cria esse nome sozinho, e isso importa: nomes de cache e nomes definidos compartilham um namespace, então uma pasta de trabalho que já tenha um nome chamado Slicer_Region colidiria de outro jeito no próximo save. Se você constrói tabelas e validação juntas, as interações de nomes definidos no artigo sobre validação de dados, AutoFilter e tabelas cobrem os casos vizinhos. Compartilhamento segue a mesma regra que o Excel usa. TXLSXSlicerCaches.FindForTableColumn(ATableId, ATableColumnId) é consultado antes de um cache novo ser criado, então duas views de slicer sobre a mesma coluna de tabela — uma na planilha de dashboard, outra na planilha de detalhe — compartilham uma única part de cache e ficam sincronizadas de filtro. Deletar uma view roda a checagem inversa em toda planilha do workbook e derruba o cache mais o nome #N/A reservado dele só quando a última view que o referencia sumiu

Como ids de relacionamento ficam estáveis nos dois lados?

Ficam estáveis porque a HotXLS calcula o rid do slicer reproduzindo a ordem exata em que emite todo outro relacionamento, e adiciona os relacionamentos novos na cauda. Nada que já está no arquivo se move. Isso parece over-engineering até você debugar a alternativa. Ids de relacionamento num .rels de worksheet são referenciados de dentro da part da worksheet — por hyperlinks, pelo drawing, por comentários, por parts de tabela — e a ECMA-376 Parte 2 os resolve posicionalmente dentro do grafo de relacionamento, não por identidade estável. Se o writer inserisse o relacionamento do slicer no meio, toda referência depois dele desloca um, e a planilha silenciosamente começa a apontar para a part errada. Então XlsxWorksheetSlicerRelNumber percorre a cadeia determinística inteira — hyperlinks externos, depois o VML de comentários, comentários, imagens de header/footer, threaded comments, o drawing, o background da planilha, configurações de impressão, tabelas, tabelas dinâmicas, query tables, células únicas de tabela — e só então reivindica o próximo número para o slicer. XlsxWorkbookSlicerCacheFirstRid faz o mesmo no lado do workbook: sheets, sharedStrings, styles, VBA, external links, theme, connections, pivot caches, person, calcChain, sheet metadata, XML maps, custom XML, e então a sequência de slicerCache. O problema geral é trabalhado no texto sobre resolução de relacionamentos OPC. A extensão do lado do workbook é então construída a partir desses números por XlsxBuildWorkbookSlicerCachesExtXml, e se o arquivo de origem já tinha blocos de extensão slicerCaches, eles são removidos do extLst cru primeiro, para que o modelo os re-emita contra os rids atuais em vez de deixar uma cópia velha para trás

Como a HotXLS mantém ids de relacionamento de slicer estáveis: XlsxWorksheetSlicerRelNumber percorre a cadeia determinística da worksheet de hyperlinks externos pelo drawing até células únicas de tabela e adiciona o relacionamento do slicer na cauda, enquanto XlsxWorkbookSlicerCacheFirstRid faz o mesmo no lado do workbook, então inserir nunca desloca uma referência de rid existente
Ids de relacionamento resolvem posicionalmente, então o relacionamento do slicer é adicionado na cauda da ordem determinística de emissão nos dois lados — uma inserção no meio deslocaria toda referência posterior e apontaria a planilha para a part errada

A âncora AlternateContent e o fallback legado

A âncora de drawing é o único lugar onde o markup do slicer fica genuinamente estranho, e a estranheza é deliberada. A HotXLS emite a shape dentro de um elemento mc:AlternateContent pela ECMA-376 Parte 3, com um ramo Choice que o Excel 2013 em diante toma e um ramo Fallback para tudo mais antigo

// O que XlsxSlicerAnchorPayloadXml produz, estruturalmente:
//
//   <mc:AlternateContent>
//     <mc:Choice xmlns:sle15=".../drawing/2012/slicer" Requires="sle15">
//       <xdr:graphicFrame>
//         <a:graphicData uri=".../drawing/2010/slicer">
//           <sle:slicer xmlns:sle=".../drawing/2010/slicer" name="Region"/>
//         </a:graphicData>
//       </xdr:graphicFrame>
//     </mc:Choice>
//     <mc:Fallback><xdr:sp>...notice shape...</xdr:sp></mc:Fallback>
//   </mc:AlternateContent>

Olhe para os dois namespaces. O token Requires="sle15" resolve para o namespace de slicer de 2012, que é o portão de versão — um leitor que não entende slicers de 2012 deve pular o ramo Choice inteiro. Mas o graphicData/@uri e o elemento sle:slicer dentro dele usam o namespace 2010, porque esse é o identificador do lado do drawing que o graphic frame sempre usou. Inverter esses dois produz um arquivo que o Excel abre sem reclamar e depois renderiza como um frame vazio, que é um sintoma memoravelmente inútil. O ramo Fallback guarda um retângulo simples com um corpo de texto explicando que slicers de tabela não são suportados nesta versão — é isso que um leitor da era 2007 desenha no lugar do controle, e é por que um Excel antigo consegue fazer round-trip da pasta de trabalho sem destruir nada. TXLSXSlicer expõe a geometria como uma caixa inclusiva baseada em 1 por SetBounds(ARow1, ACol1, ARow2, ACol2) e as propriedades individuais FromRow / ToCol, mais AnchorType para o comportamento usual de mover-e-redimensionar-com-células. HasAnchor é False num caso específico: um slicer aninhado dentro de uma group shape. Esses se reproduzem com a âncora do grupo deles verbatim, e a HotXLS deliberadamente não sintetiza uma segunda âncora para eles, porque fazer isso emitiria a shape duas vezes

Editando um slicer existente

var
  Slicer: TXLSXSlicer;
  Cache: TXLSXSlicerCache;
begin
  Slicer := Sheet.Slicers.FindByName('Region');
  if Slicer <> nil then
  begin
    Slicer.SetBounds(2, 8, 16, 11);      // baseado em 1, inclusivo
    Slicer.ShowCaption := 1;             // -1 = atributo omitido
    Slicer.LockedPosition := 1;
    Slicer.RowHeightEmu := 19 * XlsxEmuPerPoint;   // 241300 EMU

    Cache := Slicer.Cache;
    if Cache <> nil then
    begin
      Cache.SortOrder   := 'descending';
      Cache.CrossFilter := 'showItemsWithNoData';
    end;
  end;

  // Remover a última view sobre uma coluna derruba o cache
  // e o nome definido #N/A reservado junto
  Sheet.Slicers.Delete(Sheet.Slicers.IndexByName('Region'));

As propriedades inteiras triestado merecem uma nota, porque parecem um acidente de modelagem e não são. ShowCaption, LockedPosition, ColumnCount, StartItem e CustomListSort todas usam -1 para significar que o atributo estava ausente no arquivo de origem e deve continuar ausente na escrita, com 0 e 1 como valores explícitos. O Excel trata ausente e explicitamente-default de forma diferente em alguns lugares, e preservar essa distinção é o que mantém um diff de bytes de um save intocado limpo. RowHeight e ColumnWidth são mantidos como strings cruas por uma razão relacionada: o Excel escreve decimais nesses atributos EMU, e reanalisá-los por um inteiro perderia precisão no round-trip. RowHeightEmu é o accessor de conveniência inteiro sobre o mesmo storage, com XlsxSlicerDefaultRowHeight em 241300 EMU — 19 pontos na razão XlsxEmuPerPoint de 12700

Onde isso para

Dois limites honestos. Primeiro, a HotXLS modela o slicer de tabela completamente mas não modela criação de slicer de pivô — você consegue abrir uma pasta de trabalho com slicers de pivô, ler a identidade deles, mantê-los intactos num save e deletá-los, mas o lado do cache se reproduz como bytes e não há API para construir um do zero. Segundo, um slicer não filtra nada sozinho. É uma UI sobre uma coluna de tabela, e o estado de linhas visíveis que ele dirige é do lado do Excel; a HotXLS escreve o controle e o binding, não um resultado de filtro computado. Se você precisa de linhas realmente filtradas no arquivo salvo, configure também o AutoFilter da tabela. Qualquer atributo que a HotXLS não modela em <slicer> ou <x15:tableSlicerCache> é carregado inalterado, então um slicer vindo de um build mais novo do Excel ainda re-serializa com os atributos desconhecidos intactos mesmo depois de você movê-lo. Essa é a propriedade que torna seguro o caminho de dirty-rebuild: reconstruir a partir do modelo só é aceitável se o modelo é um superset do que analisou. O mecanismo de extensão marcado por GUID da ECMA-376 Parte 1 §18.2.10, e as definições de part de slicer em [MS-XLSX], são o que tornam essa garantia expressável em primeiro lugar

Slicers de tabela vêm no HotXLS Delphi Component, que lê e escreve XLSX, XLSM e workbooks BIFF legados de Delphi 5 até o release atual, mais C++Builder e Free Pascal, sem instalação do Excel e sem OLE. Referência completa da API e um build de avaliação estão na página do produto HotXLS Delphi Spreadsheet Component