Artículo técnico

HotXLS Delphi Component: template-based report generation in Delphi

La forma fiable de producir un informe de Excel con estilo desde Delphi es partir de un libro de trabajo que un diseñador ya construyó. Alguien de finanzas diseña la factura en Excel: el logotipo, las cabeceras de columna, los bordes de la banda de detalle, la fila de totales en negrita, los formatos de moneda. Su código abre ese archivo, vierte datos en vivo en las celdas que el diseñador reservó para ello, y guarda el resultado. El aspecto es de ellos; los números son suyos. HotXLS, una biblioteca nativa de Delphi y C++Builder que lee y escribe libros de trabajo XLS y XLSX sin controlar Excel, le da las tres operaciones que necesita este enfoque: buscar una celda por su texto, copiar un rango con sus estilos y fórmulas intactos, e insertar filas para que todo lo de debajo se desplace hacia abajo junto con los datos

La única regla que separa a un generador que sobrevive a las ediciones de plantilla de uno que se rompe con la primera es no dirigirse nunca a las celdas por números literales de fila y columna. Una plantilla es un documento que otras personas editan. El equipo de finanzas añade una línea de impuestos, aumenta la altura de la fila del logotipo, reordena el bloque de dirección, y el formato de archivo no le ayuda en absoluto: un guardado BIFF u OOXML tiene éxito tanto si la fila 10 sigue significando lo que significaba el trimestre pasado como si no. Un generador que escribe la primera línea de detalle en una fila 10 codificada de forma fija, la primera vez que alguien inserta un bloque encima de la sección de detalle, estampará las líneas de artículo sobre las celdas equivocadas y sumará un rango de totales que ya no cubre los datos. Nada lanza una excepción, cada guardado devuelve éxito, y la única señal es un cliente que se da cuenta de una factura incorrecta

Diagrama del pipeline de plantillas de HotXLS en Delphi: anclar tokens con FindText, expandir la banda de detalle, verificar el total calculado, y después guardar
La generación de informes por plantilla en Delphi corre como cuatro etapas HotXLS: anclar los tokens, expandir la banda de detalle, verificar el total calculado y después entregar

Ancle cada coordenada a un token de marcador de posición

La solución es hacer que la plantilla lleve sus propias coordenadas. El diseñador escribe tokens como {{CUSTOMER}}, {{DATE}}, y {{DETAIL_START}} en las celdas que el generador debe tocar, y el generador calcula cada posición en tiempo de ejecución a partir de dónde encuentra esos tokens. Las ediciones de diseño ya no importan, porque el token se mueve con la celda en la que está. La segunda mitad del contrato es la regla de fallo: si falta un token obligatorio, el job se detiene antes de que cualquier dato de cliente llegue al archivo. Una plantilla que se ha desviado debería producir un ticket de job fallido, no un documento entregado

Encontrar los tokens: FindText y ReplaceText

Ambas familias de clases de HotXLS exponen búsqueda a nivel de hoja de cálculo. FindText devuelve la fila y la columna de la primera celda cuyo texto coincide, con una sobrecarga que añade sensibilidad a mayúsculas y minúsculas. ReplaceText sustituye cada ocurrencia y devuelve cuántas cambió. Las dos cubren los dos tipos de token que suele haber. Un ancla única como el nombre del cliente se localiza una vez y se escribe al lado; un token que debería aparecer exactamente una vez, como la fecha del informe, se reemplaza y se comprueba el recuento. En el lado XLSX, un relleno que se ancla de esta forma se ve así:

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  R, C: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('invoice-template.xlsx') <> 1 then
      raise Exception.Create('Cannot open invoice template');
    Sheet := Book.Sheets[0];               // TXLSXSheets.Items es de base 0

    if not Sheet.FindText('{{CUSTOMER}}', R, C) then
      raise Exception.Create('Template drift: {{CUSTOMER}} anchor missing');
    Sheet.Cells[R, C].Value := 'ACME Corp';

    if Sheet.ReplaceText('{{DATE}}',
         FormatDateTime('yyyy-mm-dd', Date)) = 0 then
      raise Exception.Create('Template drift: {{DATE}} token missing');
    // la expansión de detalle y el guardado siguen más abajo
  finally
    Book.Free;
  end;
end;

Dos detalles importan. Primero, FindText y ReplaceText coinciden con el valor de texto de una celda; un token incrustado dentro de una cadena de fórmula les resulta invisible, así que los tokens de marcador de posición pertenecen a celdas simples, nunca dentro de fórmulas. Segundo, el recuento de reemplazos es su detector de desviación. Una plantilla que debería contener exactamente un token {{DATE}} pero reporta cero reemplazos ha sido editada, y lanzar una excepción en ese momento es precisamente lo que convierte una desviación de diseño silenciosa en un fallo visible

Clonar la fila de detalle sin perder estilos ni fórmulas

La sección de detalle de una factura crece con los datos. Escribir valores directamente en filas en blanco debajo de la línea de muestra desecha todo lo que el diseñador preparó: los bordes, los formatos numéricos, las fórmulas por fila. El patrón que conserva todo eso es dejar una fila de muestra completamente formateada en la plantilla y clonarla para cada artículo. CopyRange duplica estilos y fórmulas en una sola llamada, tras lo cual el generador solo sobrescribe las celdas de valor

Diagrama de los anclajes de tokens en una plantilla HotXLS de Delphi donde un marcador ausente hace fallar el trabajo antes de escribir cualquier dato
Los tokens de plantilla llevan sus propias coordenadas, y un token ausente detiene el trabajo antes de que se escriba cualquier dato
const
  DetailRow = 10;            // la fila de muestra formateada en la plantilla
var
  I: Integer;
begin
  // Primero abre espacio antes del bloque de totales, para que el rango SUM
  // debajo de la banda de detalle se estire junto con los datos.
  if Length(Items) > 1 then
    Sheet.InsertRows(DetailRow + 1, Length(Items) - 1);

  for I := 0 to High(Items) do
  begin
    if I > 0 then              // clona estilos y fórmulas desde la fila de muestra
      Sheet.CopyRange(DetailRow, 1, DetailRow, 5, DetailRow + I, 1);
    Sheet.Cells[DetailRow + I, 1].Value := Items[I].Name;
    Sheet.Cells[DetailRow + I, 2].Value := Items[I].Qty;
    Sheet.Cells[DetailRow + I, 3].Value := Items[I].UnitPrice;
    Sheet.Cells[DetailRow + I, 4].Formula :=
      Format('B%d*C%d', [DetailRow + I, DetailRow + I]);  // sin el prefijo '='
  end;
end;

Observe con atención la asignación de fórmulas. La propiedad Formula de XLSX toma la expresión sin un signo igual inicial, mientras que la fachada XLS espera '=B10*C10' asignado mediante Value. Mezclar las dos convenciones es el error de portado más común entre las familias de clases, y falla sin quejarse: la celda simplemente contiene una cadena literal que Excel muestra como texto. Si la plantilla decora la banda de detalle con filas de título combinadas, recuerde que solo la celda superior izquierda de un área combinada lleva un valor. Las reglas de diseño del artículo complementario sobre celdas combinadas en plantillas de informe basadas en diseño explican por qué las regiones combinadas pertenecen enteramente fuera de la banda de datos

Qué mueve InsertRows, y qué deja atrás

Insertar filas antes del bloque de totales es lo que mantiene un rango SUM estirándose a medida que crece la sección de detalle. En el lado XLSX, InsertRows arrastra una larga lista de estructuras dependientes junto con las celdas: rangos combinados, alturas de fila, hipervínculos, comentarios, paneles inmovilizados, rangos de autofiltro, formatos condicionales, validaciones de datos, tablas, nombres definidos, y anclajes de imagen y de gráfico. Hay un límite en esa lista que merece la pena memorizar. La reescritura de fórmulas solo alcanza a las referencias dentro de la misma hoja. Una fórmula en una hoja de resumen que apunta a la región movida conserva sus coordenadas antiguas y lee en silencio las celdas equivocadas, razón por la cual los totales extraídos entre hojas son más seguros expresados mediante nombres a nivel de libro de trabajo. El artículo complementario sobre nombres definidos y fórmulas entre hojas desarrolla ese patrón

El formato XLS heredado traza la línea en un lugar más duro. HotXLS mantiene las tablas dinámicas, las tablas de consulta y las conexiones de datos externas en archivos BIFF como bloques de bytes en bruto. Sobreviven a abrir y guardar sin cambios, pero no están modeladas, así que la inserción de filas nunca las toca. Una plantilla que aparca una tabla dinámica debajo de un bloque de detalle en expansión se guarda sin ningún aviso mientras el rectángulo de origen de la tabla dinámica se desvía de los datos. La salida es estructural, no defensiva: mantenga el contenido de tablas dinámicas y de consulta en hojas en las que el generador nunca inserta, y la desactualización no puede ocurrir

Diagrama de lo que mueve HotXLS InsertRows en XLSX y los límites de fórmulas entre hojas y tablas dinámicas BIFF que los generadores Delphi deben respetar
InsertRows traslada hacia abajo las estructuras dependientes en XLSX, mientras que las fórmulas entre hojas y los bloques BIFF en bruto marcan los límites

Recalcule antes de la entrega, o sepa por qué se lo saltó

HotXLS no evalúa las fórmulas durante SaveAs. Cuando una persona abre el archivo, Excel recalcula todo (la fachada XLS expone CalculationMode y RecalcOnSave si necesita dirigir eso), así que un informe destinado a la bandeja de entrada de una persona no necesita nada más de usted. El panorama cambia en el momento en que el libro de trabajo alimenta a otro programa. La exportación a CSV escribe las fórmulas como su texto literal y nunca las calcula, y cualquier analizador posterior que confíe en los valores en caché leerá números obsoletos o celdas en blanco. Para esas rutas, calcule en el servidor con Calculate, que evalúa una expresión arbitraria contra el libro de trabajo cargado y devuelve el resultado:

var
  Total: Variant;
  LastDetail: Integer;
begin
  LastDetail := DetailRow + Length(Items) - 1;
  Total := Book.Calculate(Format('SUM(Invoice!D%d:D%d)',
    [DetailRow, LastDetail]));
  if (not VarIsNumeric(Total)) or
     (Abs(Total - ExpectedTotal) > 0.005) then
    raise Exception.Create('Invoice total does not match the order record');

  if Book.SaveAs('invoice-2026-0611.xlsx') <> 1 then
    raise Exception.Create('Save failed: check output path and permissions');
end;

Comprobar el total calculado contra el registro del pedido antes del guardado es un seguro barato con buen rendimiento. Convierte una factura incorrecta en un job fallido. Un operador puede reintentar un job fallido en segundos; una factura incorrecta que ya está en el buzón de un cliente le cuesta a un gestor de cuentas una disculpa y una corrección

Dos familias de clases, un algoritmo

La misma lógica se traslada entre formatos, pero no el mismo código. TXLSWorkbook para el .xls heredado se basa en interfaces y tiene recuento de referencias, con indexación de hoja en base 1, y nunca lo libera a mano. TXLSXWorkbook para .xlsx es un objeto sencillo que debe liberar en un try..finally, con indexación de hoja en base 0 y la convención de fórmulas mostrada arriba. FindText, ReplaceText, CopyRange, e InsertRows viven en ambos lados, así que la forma ancla-clona-recalcula se traslada limpiamente. El consejo práctico es comprometerse con un formato por pipeline, o esconder los dos ciclos de vida de objeto detrás de un adaptador propio y delgado en lugar de esparcir la diferencia por todo el generador

El tamaño rara vez importa para el tipo de informe que produce este patrón. Clonar una fila con estilo unos pocos miles de veces no es nada para el hardware actual. La ruta de guardado solo se convierte en el cuello de botella cuando una banda de detalle llega a seis cifras de filas, y en ese punto activar StreamingWrite envía el XML de la hoja directamente al paquete de salida en lugar de almacenarlo en búfer; el artículo sobre escrituras en streaming para jobs por lotes en el servidor cubre cuándo merece la pena hacer esa concesión. Los gráficos se comportan igual que el resto del diseño: en el lado XLSX tanto el anclaje del gráfico como sus referencias de serie se mueven cuando InsertRows se ejecuta por encima de ellos, así que un gráfico debajo de la fila de totales sigue vinculado a los datos correctos, mientras que en el lado XLS los gráficos se sitúan en sus propias hojas de gráfico y, como las tablas dinámicas, nunca se desplazan. Ese es un argumento más para mantener las hojas de presentación alejadas de la hoja que expande el generador

Este enfoque de ancla-clona-recalcula deja que un diseñador sea dueño de cómo se ve un libro de trabajo mientras su código es dueño de lo que dice, que es normalmente lo que hace que valga la pena mantener una salida de Excel generada. Las llamadas de búsqueda, copia e inserción mostradas aquí, junto con el motor de fórmulas usado para la comprobación de total previa a la entrega, se distribuyen con HotXLS Delphi Component para Delphi y C++Builder