Artículo técnico

Exportar resultados de base de datos Delphi a reportes de Excel con HotXLS

Un usuario de finanzas abre la exportación de pedidos de anoche, selecciona la columna Amount y la barra de estado de Excel muestra un conteo pero no una suma. Cada valor de la columna es texto. El código de exportación llamó AsString en cada campo porque era la forma más rápida de sacar datos de un TDataset, y el libro almacenó fielmente cuarenta mil cadenas alineadas a la izquierda que solo parecen moneda. No se produjo ninguna excepción, el archivo abre limpio y aun así el reporte es inútil para análisis. La mayoría de los defectos de base de datos a Excel se ven exactamente así: silenciosos, relacionados con tipos e invisibles hasta que un cliente intenta hacer aritmética con el resultado.

HotXLS es una biblioteca nativa de hojas de cálculo en Object Pascal que escribe archivos XLS y XLSX directamente desde Delphi y C++Builder, sin automatización de Excel. Ofrece dos rutas distintas desde un TDataset hacia un libro: el componente directo TDataToXLS y un bucle escrito a mano contra la API del libro. Las dos rutas no son intercambiables: el componente es un ciudadano VCL construido sobre la fachada XLS, así que la elección correcta depende de dónde corre el código y qué formato de archivo espera el consumidor. Este artículo cubre ambas, dónde se detiene la ruta del componente y cómo mantener intactos los tipos de campo en cualquiera de las dos.

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

Antes de cualquier llamada de API, decidan cómo aterriza cada tipo de campo Delphi en una celda. Una celda que recibe una cadena Delphi permanece como cadena; HotXLS no adivina que '1,234.50' debía ser un número, y no debería hacerlo, porque el reanálisis dependiente de locale es exactamente como una coma decimal alemana se convierte en separador de miles en un servidor en inglés. El patrón confiable es asignar mediante accesores tipados: AsFloat o AsCurrency para campos numéricos, AsDateTime para fechas, de modo que la celda contenga una serie de fecha real de Excel en lugar de una cadena formateada, y AsString solo para campos que realmente son texto.

El manejo de NULL merece una decisión explícita en lugar de un valor predeterminado. Convertir un valor de campo con VarToStr transforma SQL NULL 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 consumidores de tablas dinámicas. Para columnas de dinero, decidan antes de escribir el bucle si NULL significa cero o desconocido, porque los dos se renderizan igual cuando alguien formatea la columna, y la diferencia cambia cada agregado calculado aguas abajo.

La ruta del componente: TDataToXLS en aplicaciones VCL

Para una aplicación VCL clásica con una consulta ya conectada a un data module, TDataToXLS es la ruta de una sola llamada. Recorre cualquier descendiente de TDataset, FireDAC, ADO, IBX, cualquier cosa que implemente la interfaz abstracta de dataset, y produce una hoja con estilo que incluye captions de encabezado, fuentes, bordes, subtotales opcionales por grupo y división automática de hojas para conjuntos de resultados grandes.

var
  Exporter: TDataToXLS;
begin
  Exporter := TDataToXLS.Create(nil);
  try
    Exporter.Dataset := OrdersQuery;          // any TDataset descendant
    Exporter.WorksheetName := 'Orders';
    Exporter.HeaderSource := hsDisplayLabel;  // captions, not raw column names
    Exporter.GroupFields.Add('CustomerID');   // subtotal block per customer
    Exporter.RowsPerSheet := 50000;           // stay below the BIFF8 row ceiling
    Exporter.OnlyVisible := True;             // respect Field.Visible
    Exporter.SaveDatasetAs('orders.xls');
  finally
    Exporter.Free;
  end;
end;

Dos propiedades cargan la mayor parte del peso de producción aquí. HeaderSource := hsDisplayLabel escribe el DisplayLabel de cada campo en lugar del nombre crudo de columna SQL, así que el libro 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; establecerlo en 50,000 divide un conjunto grande entre hojas antes de que el techo del formato lo trunque. La apariencia se maneja con HeaderFont, DetailFont, GroupColor y las propiedades de estilo de borde, y el conjunto DisableFormat desactiva categorías completas de formato cuando el consumidor quiere celdas simples. Para cualquier necesidad específica, los eventos AfterCell y AfterRow entregan el rango recién escrito para postprocesamiento.

Dónde se detiene el componente

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

  • Es un componente VCL en todo sentido. Su unidad trae Forms, Controls y Dialogs, así que vincularlo a un job de consola o a un servicio Windows arrastra VCL al binario. Las unidades centrales de libro no tienen tal dependencia: solo necesitan Windows, Classes, SysUtils y Variants, por eso el código del lado servidor debe usar el bucle que se muestra abajo.
  • Está construido sobre la fachada XLS. El componente puebla un IXLSWorkbook y escribe .xls (BIFF8). No hay una propiedad que lo cambie a salida OOXML.
  • Sus eventos hablan el dialecto XLS. El parámetro Cell: IXLSRange en AfterCell pertenece al modelo de objetos XLS, así que la personalización por celda escrita ahí es código estilo XLS incluso si después el archivo se convierte a .xlsx.

Producir .xlsx a partir de la salida del componente

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

uses lxXlsxExport;

Exporter.SaveDatasetAs('orders.xls');
// the component exposes the IXLSWorkbook it populated
SaveXLSWorkbookAsXLSX(Exporter.Workbook, 'orders.xlsx');

Traten el puente como portador de datos tabulares, no como 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, y 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 reporte con estilo no lo es, y la solución honesta es generar el XLSX directamente en lugar de parchar el archivo convertido.

El bucle escrito a mano para servicios y jobs batch

El código del lado servidor debe apuntar directamente a TXLSXWorkbook. Noten la diferencia de vida útil entre las dos fachadas antes de copiar cualquier ejemplo: el TXLSWorkbook del lado XLS se mantiene mediante 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;  // stream sheet XML straight into the zip
    Book.SaveAs(FileName);
  finally
    Book.Free;
  end;
end;

Las líneas que importan son las asignaciones tipadas y la guardia IsNull: las fechas llegan como series de fecha, los importes llegan como doubles y las fechas de pedido NULL permanecen realmente vacías en lugar de convertirse en cadenas vacías. StreamingWrite := True cambia solo la ruta de guardado: el XML de la hoja 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 un overload TStream, así que el libro puede ir directo a una respuesta HTTP sin tocar disco; el artículo de streaming write y jobs batch recorre ese patrón de despliegue, y el artículo de rendimiento de libros grandes cubre qué hacer cuando los conteos de filas suben más.

Una nota práctica más: el límite de la cuadrícula XLSX es 1,048,576 filas por 16,384 columnas, así que la lógica de división de hojas que RowsPerSheet aporta del lado XLS rara vez se necesita aquí; pero un libro de un millón de filas rara vez es lo que un consumidor humano quiere. Cuando el conjunto de resultados es tan grande, un archivo delimitado suele ser el mejor contrato; el artículo de exportación CSV y TSV cubre delimitadores, comportamiento de BOM y la advertencia de evaluación de fórmulas que aplica allí.

Preguntas comunes sobre exportación de datasets

¿TDataToXLS puede escribir .xlsx directamente?

No. El componente está construido sobre la fachada XLS y escribe archivos BIFF8. O convierten su libro con SaveXLSWorkbookAsXLSX y aceptan los límites de fidelidad descritos arriba, o escriben el bucle XLSX a mano; los proyectos demo incluyen ambos patrones uno junto al otro.

¿El servidor necesita Microsoft Excel instalado?

No. Ambos motores son escritores nativos de Object Pascal, flujos de registros BIFF8 de un lado, zip OOXML más XML del otro, así que no hay automatización COM, no hay licencia de Excel en el servidor y no hay cuello de botella de instancia única cuando varias exportaciones corren simultáneamente. Cada hilo debe usar simplemente su propia instancia de libro, porque los objetos de libro no son thread-safe para uso compartido.

¿Por qué mis números exportados no suman en Excel?

Casi siempre porque se escribieron mediante AsString o un Variant que llegó como texto. Asignen campos numéricos con AsFloat o AsCurrency y dejen que los formatos numéricos manejen la presentación; entonces la celda lleva un double IEEE real que todo agregado de Excel entiende.

Elegir un punto de partida

Si la exportación vive en una herramienta de escritorio VCL y la salida .xls es aceptable, empiecen con TDataToXLS y su soporte de agrupación: es la menor cantidad de código. Si el código corre desatendido, o el consumidor requiere .xlsx, escriban el bucle. Ambas rutas, con proyectos demo funcionales para cada una, forman parte del paquete HotXLS Component.