Artículo técnico

Exportar datasets de Delphi a informes de Excel con HotXLS

Convertir el resultado de una consulta en un informe de Excel son tres problemas con un mismo abrigo. Cada tipo de campo de Delphi tiene que aterrizar en una celda como el tipo correcto de Excel, la fila de encabezado tiene que leerse como un informe y no como un volcado de esquema, y los números, las fechas y el dinero tienen que llevar formatos que sobrevivan al viaje. Omita cualquiera de los tres y el archivo igual se abre, igual parece plausible, e igual falla en el momento en que un usuario de finanzas selecciona una columna y espera una suma que nunca aparece. Los valores se escribieron como texto, Excel los trata como etiquetas, y nunca se lanzó ninguna excepción para avisarle

HotXLS es una biblioteca de hojas de cálculo nativa en Object Pascal que escribe archivos XLS y XLSX directamente desde Delphi y C++Builder, sin automatización de Excel de por medio. Ofrece dos rutas de un TDataset a un libro de trabajo: el componente listo para usar TDataToXLS, y un bucle escrito a mano contra la API del libro de trabajo. No son intercambiables. El componente es un ciudadano de la VCL construido sobre la fachada XLS, así que la elección correcta depende de dónde se ejecuta el código y qué formato de archivo espera el consumidor. Lo que sigue son ambas rutas, la línea donde el componente deja de ser la herramienta correcta, y cómo mantener intactos los tipos de campo sea cual sea la que elija

Diagrama de dos rutas de exportación de HotXLS desde un TDataset de Delphi: el componente VCL TDataToXLS escribiendo archivos BIFF8 y un bucle TXLSXWorkbook escrito a mano para XLSX
TDataToXLS es la ruta de una sola llamada para herramientas de escritorio VCL que escriben .xls, mientras que el bucle TXLSXWorkbook escrito a mano sirve para trabajos desatendidos y .xlsx nativo

Los tipos de campo son el verdadero contrato de exportación

Antes de cualquier llamada a la API, decida cómo aterriza cada tipo de campo de Delphi en una celda. Una celda que recibe una cadena de Delphi sigue siendo una cadena. HotXLS no adivina que '1,234.50' pretendía ser un número, y no debería, porque el reanálisis dependiente de la configuración regional es exactamente la forma en que una coma decimal alemana se convierte en separador de miles en un servidor en inglés. El patrón confiable es asignar a través de los accesores con tipo: AsFloat o AsCurrency para campos numéricos, AsDateTime para fechas de modo que la celda contenga un serial de fecha genuino de Excel y no una cadena formateada, y AsString solo para campos que realmente son texto

El manejo de nulos merece una decisión explícita en lugar de un valor predeterminado. Convertir un valor de campo con VarToStr transforma el NULL de SQL en una cadena vacía, que es una celda de texto, mientras que omitir la asignación deja la celda verdaderamente vacía, que es lo que esperan AVERAGE, COUNT y los consumidores de tablas dinámicas. Para las columnas de dinero, decida antes de escribir el bucle si NULL significa cero o desconocido. Los dos se renderizan idénticos en cuanto alguien da formato a la columna, y la diferencia cambia cada agregado calculado más adelante

Diagrama que asigna los accesores de campo de dataset de Delphi a tipos de celda de Excel con HotXLS, contrastando el manejo de NULL con VarToStr frente a una celda genuinamente vacía
El contrato de exportación es el tipo de campo: los accesores con tipo depositan números y fechas como valores reales de Excel, mientras que VarToStr convierte silenciosamente el NULL de SQL en una celda de texto

La ruta del componente: TDataToXLS en aplicaciones VCL

Para una aplicación VCL clásica con una consulta ya conectada en un módulo de datos, TDataToXLS es la ruta de una sola llamada. Recorre cualquier descendiente de TDataset, ya sea FireDAC, ADO, IBX o cualquier otro que implemente la interfaz abstracta de dataset, y produce una hoja de cálculo con estilo, con títulos de encabezado, fuentes, bordes, subtotales de grupo opcionales y división automática en hojas para conjuntos de resultados grandes

var
  Exporter: TDataToXLS;
begin
  Exporter := TDataToXLS.Create(nil);
  try
    Exporter.Dataset := OrdersQuery;          // cualquier descendiente de TDataset
    Exporter.WorksheetName := 'Orders';
    Exporter.HeaderSource := hsDisplayLabel;  // títulos, no nombres de columna en bruto
    Exporter.GroupFields.Add('CustomerID');   // bloque de subtotal por cliente
    Exporter.RowsPerSheet := 50000;           // por debajo del techo de filas de BIFF8
    Exporter.VisibleFieldsOnly := True;             // respeta Field.Visible
    Exporter.SaveDatasetAs('orders.xls');
  finally
    Exporter.Free;
  end;
end;

Dos propiedades cargan con la mayor parte del peso de producción aquí. HeaderSource := hsDisplayLabel escribe el DisplayLabel de cada campo en lugar del nombre de columna SQL en bruto, así que el libro de trabajo dice "Customer Name" en lugar de CUST_NM. RowsPerSheet existe porque el componente escribe BIFF8, cuya cuadrícula se detiene en 65 536 filas por 256 columnas; fijarlo en 50 000 divide un conjunto de resultados grande entre varias hojas antes de que el techo del formato lo trunque. La apariencia se maneja con las propiedades HeaderFont, DetailFont, GroupColor y las de estilo de borde, y el conjunto DisableFormat desactiva categorías enteras de formato cuando el consumidor quiere celdas simples. Para cualquier cosa a medida, los eventos AfterCell y AfterRow le entregan el rango recién escrito para posprocesarlo

Dónde se detiene el componente

Tres restricciones están diseñadas dentro de TDataToXLS, y conocerlas de antemano evita un rediseño incómodo dos sprints después

Diagrama que contrasta las unidades VCL que TDataToXLS arrastra a un binario de Delphi con las cuatro unidades RTL que necesita el código central de libro de trabajo de HotXLS
Enlazar TDataToXLS en un servicio arrastra Forms, Controls y Dialogs, mientras que las unidades centrales de libro de trabajo solo necesitan Windows, Classes, SysUtils y Variants
  • Es un componente VCL en el sentido pleno. Su unidad incorpora Forms, Controls y Dialogs, así que enlazarlo en un trabajo de consola o en un servicio de Windows arrastra la VCL al binario. Las unidades centrales de libro de trabajo no tienen esa dependencia. Solo necesitan Windows, Classes, SysUtils y Variants, razón por la cual el código del lado del servidor debería usar en su lugar el bucle que se muestra más abajo
  • Está construido sobre la fachada XLS. El componente puebla un IXLSWorkbook y escribe .xls (BIFF8). No hay ninguna propiedad que lo cambie a salida OOXML
  • Sus eventos hablan el dialecto XLS. El parámetro Cell: IXLSRange de AfterCell pertenece al modelo de objetos XLS, así que la personalización por celda escrita ahí es código al estilo XLS aunque el archivo se convierta después a .xlsx

Producir .xlsx a partir de la salida del componente

Cuando el consumidor insiste en .xlsx pero la lógica de exportación ya vive en TDataToXLS, la función puente de la unidad lxXlsxExport convierte el libro de trabajo poblado en una sola llamada:

uses lxXlsxExport;

Exporter.SaveDatasetAs('orders.xls');
// el componente expone el IXLSWorkbook que pobló
SaveXLSWorkbookAsXLSX(Exporter.Workbook, 'orders.xlsx');

Trate el puente como un transportador de datos tabulares, no como un conversor de fidelidad completa. Copia valores, fórmulas, formatos numéricos, colores de relleno, atributos de fuente, anchos de columna y configuración de vista. Deliberadamente no copia bordes, rangos combinados, comentarios, gráficos ni formatos condicionales. Para una cuadrícula plana de encabezado más filas eso es exactamente suficiente. Para un informe con estilo no lo es, y la solución honesta es generar el XLSX directamente en lugar de parchear el archivo convertido

El bucle escrito a mano para servicios y trabajos por lotes

El código del lado del servidor debería apuntar directamente a TXLSXWorkbook. Observe la diferencia de ciclo de vida entre las dos fachadas antes de copiar cualquier ejemplo. El TXLSWorkbook del lado XLS se sostiene a través de una interfaz con conteo de referencias y no debe liberarse manualmente, mientras que TXLSXWorkbook es una clase normal que requiere try..finally Free. Mezclar las dos convenciones es una forma confiable de fabricar una fuga o una doble liberación

procedure ExportOrders(Q: TDataSet; const FileName: string);
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Row: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Orders');
    Sheet.Cells[1, 1].Value := 'Order No';
    Sheet.Cells[1, 2].Value := 'Customer';
    Sheet.Cells[1, 3].Value := 'Ordered';
    Sheet.Cells[1, 4].Value := 'Amount';

    Row := 2;
    Q.First;
    while not Q.Eof do
    begin
      Sheet.Cells[Row, 1].Value := Q.FieldByName('OrderNo').AsInteger;
      Sheet.Cells[Row, 2].Value := Q.FieldByName('Customer').AsString;
      if not Q.FieldByName('Ordered').IsNull then
        Sheet.Cells[Row, 3].Value := Q.FieldByName('Ordered').AsDateTime;
      Sheet.Cells[Row, 4].Value := Q.FieldByName('Amount').AsFloat;
      Inc(Row);
      Q.Next;
    end;

    Book.StreamingWrite := True;  // transmite el XML de la hoja directo al zip
    Book.SaveAs(FileName);
  finally
    Book.Free;
  end;
end;

Las líneas que importan son las asignaciones con tipo y la protección con IsNull. Las fechas llegan como seriales de fecha, los importes llegan como dobles, y las fechas de pedido NULL se quedan genuinamente vacías en lugar de convertirse en cadenas vacías. StreamingWrite := True cambia solo la ruta de guardado: el XML de la hoja de cálculo se transmite directamente al contenedor zip en lugar de ensamblarse primero como una gran cadena, lo que aplana el pico de memoria en el momento de SaveAs para conteos de filas de seis cifras. Cada método de guardado también tiene una sobrecarga con TStream, así que el libro de trabajo puede ir directamente a una respuesta HTTP sin tocar el disco. El artículo sobre escritura por streaming y trabajos por lotes recorre ese patrón de despliegue, y el artículo sobre rendimiento con libros de trabajo grandes cubre qué hacer cuando los conteos de filas siguen creciendo

Este bucle es también la ruta que escala entre hilos. Ambos motores son escritores nativos en Object Pascal, flujos de registros BIFF8 de un lado y zip OOXML más XML del otro, así que ninguna parte de una exportación toca la automatización COM ni necesita una licencia de Excel en el servidor. Lo que eso le compra es paralelismo sin un cuello de botella de instancia única, siempre que cada hilo construya su propio libro de trabajo. Los objetos de libro de trabajo no son seguros para uso compartido entre hilos, así que la regla es una instancia por exportación, nunca una compartida protegida por un bloqueo

Vale la pena conocer un límite antes de diseñar alrededor de él. La cuadrícula XLSX se detiene en 1 048 576 filas por 16 384 columnas, así que la división en hojas que RowsPerSheet maneja del lado XLS rara vez se necesita aquí. Un libro de trabajo de un millón de filas rara vez es tampoco lo que quiere un consumidor humano. Cuando el conjunto de resultados es genuinamente así de grande, un archivo delimitado suele ser el mejor contrato, y el artículo sobre exportación CSV y TSV cubre los delimitadores, el comportamiento del BOM y la salvedad de evaluación de fórmulas que aplica ahí

Elegir un punto de partida

Si la exportación vive en una herramienta de escritorio VCL y la salida .xls es aceptable, empiece con TDataToXLS y su compatibilidad con agrupación. Es la menor cantidad de código, y el puente a través de SaveXLSWorkbookAsXLSX está ahí cuando alguien pida .xlsx más adelante, siempre que acepte los límites de fidelidad ya descritos. Si el código se ejecuta de forma desatendida, o el consumidor requiere .xlsx desde el principio, escriba el bucle. Ambas rutas se distribuyen con proyectos de demostración funcionales y forman parte del paquete HotXLS Delphi Component