Artículo técnico

Slicers de tabla de Excel en Delphi: HotXLS Round-Trip

HotXLS crea slicers de tabla de Excel desde Delphi y C++Builder con una sola llamada — Worksheet.Slicers.Add(Table, ColumnName) — y escribe los ocho lugares del paquete que un slicer necesita: las partes slicer y slicerCache, las dos anulaciones de content-type, las relaciones de hoja y de libro con sus bloques extLst correspondientes, y el ancla del dibujo

Un slicer tiene el aspecto de un control que sueltas en una hoja. En el archivo no es nada de eso. Es un registro distribuido repartido entre dos partes nuevas, dos grafos de relaciones, tres bloques de extensión y un ancla de dibujo, y Excel valida el conjunto como una unidad al abrir el paquete. Si falta una pieza no te queda un slicer degradado; te queda el diálogo de reparación y, después, un libro con el slicer eliminado en silencio. Ese modo de fallo es lo que hace tan tedioso emitir slicers a mano, y por eso la ingeniería interesante aquí es contabilidad, no renderizado

¿Por qué un slicer toca ocho sitios del paquete?

Porque un slicer son tres conceptos separados que OPC obliga a meter en tres casas distintas: el estado del filtro (ámbito de libro), la vista (ámbito de hoja) y la forma (ámbito de dibujo). HotXLS escribe exactamente ocho puntos de aterrizaje por slicer de tabla, y ninguno sobra

  • xl/slicers/slicerN.xml — una parte por hoja de cálculo, con todas las vistas <slicer> de esa hoja en el namespace http://schemas.microsoft.com/office/spreadsheetml/2009/9/main
  • xl/slicerCaches/slicerCacheN.xml — a nivel de libro, una por columna de tabla segmentada, con el binding real
  • un <Override> en [Content_Types].xml para cada una de esas dos partes, con application/vnd.ms-excel.slicer+xml y application/vnd.ms-excel.slicerCache+xml (ECMA-376 Part 2 exige un content type para cada parte, y Excel rechaza el paquete sin ellos)
  • una relación de hoja del tipo .../office/2007/relationships/slicer, más una extensión <x14:slicerList> dentro del extLst de la hoja que apunta a ese rid
  • una relación de libro del tipo .../office/2007/relationships/slicerCache, más una extensión <x15:slicerCaches> dentro del extLst del libro que lista los mismos rids
  • un ancla mc:AlternateContent en el dibujo de la hoja que da al slicer su posición y su tamaño
  • un bloque <x15:tableSlicerCache> dentro de la parte de caché que lo enlaza con un id de tabla y un id de columna de tabla

Los bloques de extensión usan el mecanismo extLst estándar de ECMA-376 Part 1 §18.2.10: cada <ext> lleva un atributo uri que identifica la funcionalidad, y los lectores que no reconocen el GUID se saltan el bloque. HotXLS distingue los slicers de tabla de los slicers de tabla dinámica solo por ese GUID — {3A4CF648-6AED-40f4-86FF-DC5316D8AED3} para el slicerList de tabla, {A8765BA9-456A-4dab-B4F3-ACF838C121DE} para el de tabla dinámica, y en paralelo {46BE6895-7355-4a93-B00E-2C351335B9C9} y {BBE1A952-AA13-448e-AADC-164F8A28A991} para las dos extensiones slicerCaches del libro. El binding de tabla vive bajo {2F2917AC-EB37-4324-AD4E-5DD8C200BD13}. Esas constantes están expuestas en la unit como XlsxExtUriSlicerListX15, XlsxExtUriWbSlicerCachesX15 y XlsxExtUriTableSlicerCache, así que nada en el camino de escritura lleva un string mágico

Los ocho lugares del paquete que HotXLS escribe para un slicer de tabla creado con Worksheet.Slicers.Add: las partes slicer y slicerCache, dos anulaciones de content type, las relaciones de hoja y de libro, los bloques extLst x14:slicerList y x15:slicerCaches, y el ancla del dibujo, todos estructurales cuando Excel valida el paquete
Una llamada a Worksheet.Slicers.Add se despliega en ocho puntos de aterrizaje del paquete: dos partes, dos anulaciones de content-type, dos relaciones, dos bloques extLst y un ancla de dibujo, todo validado como unidad al abrir el archivo

¿Qué partes se pueden reproducir tal cual y cuáles deben reconstruirse?

HotXLS responde con una sola regla: una parte solo puede reproducir sus bytes originales si nada de lo que referencia puede renumerarse durante el guardado. Las cachés de slicers de tabla suspenden esa prueba, así que HotXLS las reconstruye a partir del modelo en cada guardado, sin excepciones — aunque el usuario nunca haya tocado el slicer. La razón es tableId: el elemento <x15:tableSlicerCache> identifica su tabla por el atributo id de <table>, y HotXLS renumera los ids de tabla a nivel global del libro al escribir. Una implementación temprana guardaba la parte de caché como bytes crudos al abrir y los volcaba de vuelta al guardar, que es la estrategia correcta para la mayoría de partes opacas. Producía archivos que abrían sin problemas en el caso normal y disparaban una reparación en el interesante: añade una tabla en una hoja anterior, guarda, y la caché apunta ahora a un id de tabla que pertenece a otra cosa, o a nada. El bug es invisible en una regresión de abrir y guardar sin más porque los ids resulta que no se mueven

Las cachés con forma de tabla dinámica toman la ruta contraria. Su contenido es <pivotTables> más un bloque <x14:data> que describe los elementos cacheados, el estado de ordenación y los índices de elemento — una estructura sin renumeración por parte de HotXLS, y cara de modelar con fidelidad. Esas se conservan como bytes crudos y se reproducen exactas, con TXLSXSlicerCache.IsPivot e IsTableCache distinguiendo los dos casos. Las vistas del slicer siguen una tercera política, el marcado de suciedad: un elemento <slicer> intacto y su ancla de dibujo se reproducen tal cual, y en cuanto se modifica cualquier vista de la hoja se regenera toda la parte de la hoja a partir del modelo. Esa es la forma general del criterio — reproducción literal donde las referencias son estables, reconstrucción desde el modelo donde el escritor reasigna identidades, marcado de suciedad donde ambas son aceptables — y aplica mucho más allá de los slicers, como desarrollan con más detalle las notas de round-trip sin pérdidas sobre temas, bloques extLst y calcChain

Política de reproducción por parte de slicer en HotXLS: las cachés de slicers de tabla siempre se reconstruyen desde el modelo porque los tableId se renumeran a nivel global del libro al guardar, las cachés de tabla dinámica reproducen sus bytes crudos porque pivotTables y x14:data nunca se renumeran, y las vistas del slicer se marcan como sucias, reproduciéndose tal cual hasta que cambia cualquier vista de la hoja
Política de reproducción por parte: las cachés de slicers de tabla siempre se reconstruyen porque los ids de tabla se renumeran a nivel de libro, las cachés de tabla dinámica reproducen bytes crudos porque nada en ellas se renumera, y las vistas se marcan como sucias para que un guardado sin tocar nada quede limpio a nivel de byte

Crear un slicer de tabla desde Delphi

TXLSXSlicers.Add es toda la API de creación. Dado un TXLSXTable y un nombre de columna, materializa los ids de las columnas de la tabla, encuentra o crea el TXLSXSlicerCache subyacente, reserva el nombre definido correspondiente y ancla una vista a la derecha de la tabla. La sobrecarga de dos argumentos elige la colocación; la de seis toma una caja inclusiva explícita basada en 1

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

    // Colocación por defecto: dos columnas libres a la derecha
    // de la tabla, 13 filas de alto a la altura de fila 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;

Merece la pena conocer dos reglas de nombres porque son convenciones de Excel, no inventos de HotXLS. La caché se llama Slicer_<column>, con los caracteres que no son identificadores convertidos en guiones bajos, y las colisiones añaden un sufijo numérico — Slicer_Region, luego Slicer_Region1, y así. Y cada nombre de caché se reserva también como nombre definido del libro cuya fórmula es #N/A, que es lo que hace TXLSXSlicers.ReserveCacheDefinedName. Excel crea ese nombre por su cuenta, y es importante: los nombres de caché y los nombres definidos comparten un único namespace, así que un libro que ya tenga un nombre llamado Slicer_Region colisionaría en el siguiente guardado. Si construyes tablas y validación juntas, las interacciones de nombres definidos del artículo sobre validación de datos, AutoFilter y tablas cubren los casos vecinos. El uso compartido sigue la misma regla que usa Excel. Se consulta TXLSXSlicerCaches.FindForTableColumn(ATableId, ATableColumnId) antes de crear una caché nueva, así que dos vistas de slicer sobre la misma columna de tabla — una en la hoja del panel, otra en la hoja de detalle — comparten una sola parte de caché y se mantienen sincronizadas en el filtro. Borrar una vista ejecuta la comprobación inversa sobre todas las hojas del libro y elimina la caché con su nombre #N/A reservado solo cuando desaparece la última vista que la referencia

¿Cómo se mantienen estables los ids de relación en ambos extremos?

Se mantienen estables porque HotXLS calcula el rid del slicer reproduciendo el orden exacto en el que emite todas las demás relaciones, y añade las nuevas al final. Nada de lo que ya está en el archivo se mueve. Suena a sobre-ingeniería hasta que has depurado la alternativa. Los ids de relación de un .rels de hoja se referencian desde dentro de la parte de la hoja — por hipervínculos, por el dibujo, por los comentarios, por las partes de tabla — y ECMA-376 Part 2 los resuelve posicionalmente dentro del grafo de relaciones, no por ninguna identidad estable. Si el escritor inserta la relación del slicer en medio, todas las referencias posteriores se desplazan una posición y la hoja empieza a apuntar silenciosamente a la parte equivocada. Así que XlsxWorksheetSlicerRelNumber recorre toda la cadena determinista — hipervínculos externos, luego el VML de comentarios, los comentarios, las imágenes de encabezado/pie, los comentarios encadenados, el dibujo, el fondo de la hoja, la configuración de impresora, las tablas, las tablas dinámicas, las query tables, las celdas únicas de tabla — y solo entonces reclama el siguiente número para el slicer. XlsxWorkbookSlicerCacheFirstRid hace lo mismo en el lado del libro: hojas, sharedStrings, estilos, VBA, vínculos externos, tema, conexiones, cachés de tabla dinámica, person, calcChain, metadatos de hoja, XML maps, XML personalizado, y después la serie de slicerCache. El problema general se trata en profundidad en el artículo sobre la resolución de relaciones OPC. La extensión del lado del libro se construye después a partir de esos números con XlsxBuildWorkbookSlicerCachesExtXml, y si el archivo fuente ya tenía bloques de extensión slicerCaches se eliminan primero del extLst crudo, para que el modelo los vuelva a emitir contra los rids actuales en lugar de dejar una copia obsoleta

Cómo mantiene HotXLS estables los ids de relación de los slicers: XlsxWorksheetSlicerRelNumber recorre la cadena determinista de la hoja desde los hipervínculos externos hasta las celdas únicas de tabla pasando por el dibujo y añade la relación del slicer al final, mientras XlsxWorkbookSlicerCacheFirstRid hace lo mismo en el lado del libro, de modo que insertar nunca desplaza una referencia rid existente
Los ids de relación se resuelven posicionalmente, así que la relación del slicer se añade al final del orden de emisión determinista en ambos lados — una inserción en medio desplazaría todas las referencias posteriores y apuntaría la hoja a la parte equivocada

El ancla AlternateContent y su fallback para versiones antiguas

El ancla del dibujo es el único sitio donde el markup del slicer se vuelve genuinamente raro, y esa rareza es deliberada. HotXLS emite la forma dentro de un elemento mc:AlternateContent según ECMA-376 Part 3, con una rama Choice que toman Excel 2013 y posteriores y una rama Fallback para todo lo anterior

// Lo que produce XlsxSlicerAnchorPayloadXml, estructuralmente:
//
//   <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>...forma de aviso...</xdr:sp></mc:Fallback>
//   </mc:AlternateContent>

Fíjate en los dos namespaces. El token Requires="sle15" resuelve al namespace de slicers de 2012, que es el gate de versión — un lector que no entienda los slicers de 2012 debe saltarse toda la rama Choice. Pero el graphicData/@uri y el elemento sle:slicer de dentro usan el namespace de 2010, porque ese es el identificador del lado del dibujo que el graphic frame ha usado siempre. Ponerlos al revés produce un archivo que Excel abre sin rechistar y luego renderiza como un marco vacío, que es un síntoma memorablemente poco útil. La rama Fallback contiene un rectángulo plano con un cuerpo de texto que explica que los slicers de tabla no se soportan en esta versión — eso es lo que dibuja un lector de la era 2007 en lugar del control, y por eso un Excel antiguo puede hacer round-trip del libro sin destruir nada. TXLSXSlicer expone la geometría como una caja inclusiva basada en 1 mediante SetBounds(ARow1, ACol1, ARow2, ACol2) y las propiedades individuales FromRow / ToCol, más AnchorType para el comportamiento habitual de mover y redimensionar con las celdas. HasAnchor es False en un caso concreto: un slicer anidado dentro de una forma de grupo. Esos se reproducen con su ancla de grupo tal cual y HotXLS deliberadamente no sintetiza un segundo ancla para ellos, porque hacerlo emitiría la forma dos veces

Editar un slicer existente

var
  Slicer: TXLSXSlicer;
  Cache: TXLSXSlicerCache;
begin
  Slicer := Sheet.Slicers.FindByName('Region');
  if Slicer <> nil then
  begin
    Slicer.SetBounds(2, 8, 16, 11);      // basado en 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;

  // Eliminar la última vista sobre una columna elimina su caché
  // y con ella el nombre definido #N/A reservado
  Sheet.Slicers.Delete(Sheet.Slicers.IndexByName('Region'));

Las propiedades enteras de tres estados merecen una nota, porque parecen un accidente del modelado y no lo son. ShowCaption, LockedPosition, ColumnCount, StartItem y CustomListSort usan todas -1 para indicar que el atributo no estaba en el archivo fuente y debe seguir ausente al escribir, con 0 y 1 como valores explícitos. Excel trata en algunos sitios de forma distinta la ausencia y el valor por defecto explícito, y preservar esa distinción es lo que mantiene limpio el diff de bytes de un guardado sin tocar. RowHeight y ColumnWidth se conservan como strings crudos por una razón emparentada: Excel escribe decimales en esos atributos EMU, y volver a parsearlos por un entero perdería precisión en el round-trip. RowHeightEmu es el accessor de conveniencia entero sobre el mismo almacenamiento, con XlsxSlicerDefaultRowHeight en 241300 EMU — 19 puntos a la ratio XlsxEmuPerPoint de 12700

Dónde llegan los límites

Dos límites honestos. Primero, HotXLS modela el slicer de tabla por completo pero no modela la creación de slicers de tabla dinámica — puedes abrir un libro con slicers de tabla dinámica, leer su identidad, mantenerlos intactos a través de un guardado y borrarlos, pero el lado de la caché se reproduce como bytes y no hay API para construir uno desde cero. Segundo, un slicer no filtra nada por sí solo. Es una UI sobre una columna de tabla, y el estado de filas visibles que maneja es cosa de Excel; HotXLS escribe el control y el binding, no un resultado de filtro calculado. Si necesitas filas realmente filtradas en el archivo guardado, configura también el AutoFilter de la tabla. Cualquier atributo que HotXLS no modele en <slicer> o <x15:tableSlicerCache> se propaga sin cambios, así que un slicer venido de una compilación más nueva de Excel se re-serializa con sus atributos desconocidos intactos incluso después de moverlo. Esa es la propiedad que hace seguro el camino de reconstrucción con marcado: reconstruir desde el modelo solo es aceptable si el modelo es un superconjunto de lo que se parseó. El mecanismo de extensión etiquetada con GUID de ECMA-376 Part 1 §18.2.10, y las definiciones de las partes de slicer en [MS-XLSX], son lo que hace expresable esa garantía en primer lugar

Los slicers de tabla se incluyen en el HotXLS Delphi Component, que lee y escribe libros XLSX, XLSM y BIFF heredado desde Delphi 5 hasta la versión actual, además de C++Builder y Free Pascal, sin instalación de Excel y sin OLE. La referencia completa de la API y una build de prueba están en la página del producto HotXLS Delphi Spreadsheet Component