El localizador suena a las 02:10. El trabajo nocturno de reportes se quedó detenido, y el Administrador de tareas del servidor muestra once procesos EXCEL.EXE pertenecientes a la cuenta de servicio, cada uno esperando un cuadro de diálogo que nadie va a presionar. Cualquiera que haya manejado Excel mediante automatización COM desde un servidor ha vivido alguna versión de esa noche. La propia guía de Microsoft ha sido directa durante dos décadas: Office no está diseñado ni licenciado para automatización desatendida del lado servidor. HotXLS existe exactamente para ese hueco: es una biblioteca nativa Object Pascal que lee y escribe directamente los formatos de archivo de hoja de cálculo, así que no hay un proceso de Excel que pueda colgarse, filtrar recursos o exigir licencia.
Por qué manejar EXCEL.EXE desde un servicio falla
La automatización COM controla remotamente una aplicación de escritorio, y una aplicación de escritorio asume cosas que un servicio de Windows no puede ofrecer: un perfil de usuario cargado, una estación de ventanas interactiva y una persona mirando la pantalla. Bajo una cuenta de servicio, esas suposiciones se rompen de formas que nunca aparecen en el equipo de desarrollo. Un aviso de recuperación de archivo, un error de complemento o un diálogo de activación de licencia aparece en un escritorio que nadie puede ver, y la llamada de automatización simplemente nunca regresa. Cuando el proceso llamador se rinde y muere, la instancia de Excel muchas veces sobrevive como huérfana, reteniendo bloqueos de archivo que también rompen la siguiente ejecución.
Incluso cuando nada se cae, el modelo escala mal. Cada instancia de Excel es efectivamente una canalización de un solo libro, el marshalling COM entre procesos agrega latencia a cada acceso de propiedad, y la máquina que ejecuta el código necesita una licencia de Office cuyo EULA dice que no cubre este uso. Los equipos suelen descubrir cada una de estas restricciones una caída a la vez, por eso "reemplazar la capa COM" termina entrando en el plan trimestral de alguien.
El reemplazo tiene una pregunta de alcance oculta que conviene resolver temprano: el código COM rara vez solo escribe celdas. Llama a Workbook.SaveAs con constantes de formato, dispara recalculación, aplica configuración de impresión y a veces maneja el portapapeles. Inventaríen esos comportamientos antes de reescribir, porque cada uno mapea a una parte distinta de la API de una biblioteca nativa; y unos pocos, como la interoperación con portapapeles, no tienen equivalente del lado servidor y no deberían reproducirse en absoluto.
Dos motores nativos, dos modelos de propiedad
HotXLS reemplaza el proceso de Excel con implementaciones directas de formato: un motor de flujo de registros BIFF8 (TXLSWorkbook, unidad lxHandle) para .xls, y un escritor de paquetes OOXML (TXLSXWorkbook, unidad lxHandleX) que produce .xlsx conforme a ECMA-376 / ISO/IEC 29500. No hay nada que registrar, nada que instalar en el servidor, y tantos libros en curso como permita la memoria.
Lo primero que deben interiorizar es que las dos fachadas siguen reglas de vida distintas, y confundirlas es el bug clásico de la primera semana:
var
Book: IXLSWorkbook; // interface reference: released automatically
Sheet: IXLSWorksheet;
BookX: TXLSXWorkbook; // plain object: you free it
SheetX: TXLSXWorksheet;
begin
// BIFF8 .xls output - no Free; the interface refcount owns it
Book := TXLSWorkbook.Create;
Sheet := Book.Sheets.Add;
Sheet.Name := 'Report';
Sheet.Cells.Item[1, 1].Value := 'Generated without Excel';
Book.SaveAs('report.xls');
// OOXML .xlsx output - explicit lifetime
BookX := TXLSXWorkbook.Create;
try
SheetX := BookX.Sheets.Add('Report');
SheetX.Cells[1, 1].Value := 'Generated without Excel';
BookX.SaveAs('report.xlsx');
finally
BookX.Free;
end;
end;
La fachada XLS se administra por conteo de referencias mediante la interfaz IXLSWorkbook: declaren la variable como tipo de interfaz y nunca llamen a Free, porque sostenerla en una variable de objeto y liberarla manualmente prepara una doble liberación. La fachada XLSX es un objeto normal con un try..finally normal. El direccionamiento de celdas es basado en 1 en ambos lados, pero las colecciones de hojas difieren: Entries del lado XLS es basado en 1, mientras que el indexador Items de XLSX es basado en 0, un off-by-one que compila limpiamente en cualquier dirección y solo falla en tiempo de ejecución.
Escribir un libro directamente en una respuesta HTTP
Las exportaciones del lado servidor normalmente no deberían tocar disco. Los archivos temporales necesitan política de limpieza, chocan entre solicitudes concurrentes y dejan datos de clientes en volúmenes que nadie audita. Ambas fachadas exponen sobrecargas de SaveAs que reciben un TStream:
Mem := TMemoryStream.Create;
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Data');
Sheet.Cells[1, 1].Value := 'Generated ' + DateTimeToStr(Now);
Book.SaveAs(Mem); // writes from the CURRENT stream position
Mem.Position := 0; // rewind before handing the stream over
Response.ContentType :=
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
Response.ContentStream := Mem; // the framework now owns Mem
finally
Book.Free;
end;
La línea que merece su comentario es el rebobinado. SaveAs(Stream) escribe desde la posición actual del stream y no vuelve a cero cuando termina. Si omiten Mem.Position := 0, el cliente recibe una descarga de cero bytes o Excel reporta un archivo corrupto; por mucho, es el error más común en código de libros orientado a web, y uno que pasa cualquier prueba unitaria que solo revise el tamaño del stream.
El mismo código de generación se ramifica a otros formatos de entrega sin reestructurarse. SaveAsCSV y SaveAsHTML cubren las solicitudes de "solo dame los datos" e "incrústalo en una página del portal" que siempre siguen a una exportación Excel exitosa, SaveAsRTF alimenta canalizaciones de documentos y SaveAsODS satisface mandatos OpenDocument, cada una con sobrecargas de archivo y stream. Un servicio que expone una sola rutina de construcción de libro y un parámetro de formato reemplaza lo que antes eran cuatro macros COM distintas, y las TXLSXHtmlExportOptions del exportador HTML (título, clase CSS, salida fragmento frente a documento) evitan que el caso del portal termine en cirugía de cadenas sobre marcado exportado.
Valores de fórmula sin un proceso Excel que los calcule
Con automatización COM, Excel recalculaba todo gratis. Un escritor nativo cambia el contrato: SaveAs almacena fórmulas sin evaluarlas, y los valores aparecen cuando Excel abre el archivo y recalcula (la fachada XLS expone RecalcOnSave y CalculationMode para ajustar ese comportamiento). Eso está bien para archivos destinados a una persona, pero un servicio que debe validar totales antes de entregar, o exportar a CSV, que emite texto de fórmula en vez de resultados, tiene que evaluar del lado servidor con el motor integrado:
SheetX.Cells[1, 1].Value := 1200;
SheetX.Cells[2, 1].Value := 950;
SheetX.Cells[3, 1].Formula := 'SUM(A1:A2)'; // XLSX facade: no '=' prefix
Total := BookX.Calculate('SUM(A1:A2)'); // evaluate on the server, now
if Total <> 2150 then
raise Exception.Create('reconciliation failed before delivery');
Vigilen aquí también la convención de fachada: el lado XLSX asigna expresiones mediante Cell.Formula sin signo igual, mientras que el lado XLS escribe fórmulas mediante Cell.Value con un '=' inicial. Portar código entre las dos con la convención equivocada almacena silenciosamente una cadena de texto que solo parece una fórmula. Cuando las fórmulas del libro necesitan llamar a lógica de negocio propia, el callback OnUserFunction permite que el motor de cálculo despache nombres de función desconocidos a código Delphi durante la evaluación: lo más cercano, en nativo, a los complementos UDF que suelen esconderse dentro de las hojas para las que se construyó un sistema de automatización COM.
Bordes de despliegue que solo aparecen en el servidor
Tres detalles suelen separar un despliegue limpio de uno confuso. Primero, el grafo de unidades: el exportador drag-and-drop de datasets TDataToXLS usa VCL Forms, Controls y Dialogs, lo cual no molesta en una herramienta de escritorio pero arrastra VCL a un servicio de consola. Las unidades centrales lxHandle y lxHandleX dependen solo de Windows, Classes, SysUtils y Variants, así que los servicios puros deberían escribir su propio bucle de dataset contra la API central en lugar de traer el componente.
Segundo, los hilos: las instancias de libro no son thread-safe, pero tampoco hay estado global compartido entre instancias, así que el patrón escalable es simplemente un objeto de libro por trabajo o por hilo worker; generación paralela de reportes que la automatización COM nunca podía ofrecer. Un manejador de solicitud que crea, llena, guarda y libera su propio libro no necesita ningún bloqueo, y el dominio de falla se reduce de "la instancia compartida de Excel quedó trabada" a "esta solicitud lanzó una excepción", que es una falla que su manejo de errores existente ya sabe reportar.
Tercero, el formato destino: TXLSWorkbook.SaveAs escribe BIFF (xlExcel97) de forma predeterminada, y convertir contenido XLS a .xlsx pasa por el puente SaveXLSWorkbookAsXLSX con fidelidad reducida. Elijan la fachada por formato destino en tiempo de diseño, no al final de la canalización.
Para la mitad de carga de datos de un proyecto típico de reemplazo, los patrones de exportación de base de datos a libro cubren tanto el componente como el bucle escrito a mano, y cuando los conteos de filas llegan a seis cifras, las técnicas de rendimiento para libros grandes hacen la diferencia entre minutos y segundos. Los reportes construidos desde diseños mantenidos por diseñadores se cubren en el recorrido de generación de reportes con plantillas.
HotXLS se entrega como código fuente Object Pascal para Delphi y C++Builder; las ediciones, licencias y la referencia completa de API están en la página de producto de HotXLS Component.