Artículo técnico

HotXLS: conservar macros VBA y vínculos externos en Delphi

Considera un trabajo que no hace casi nada: abrir un libro de trabajo mensual, escribir la fecha de hoy en una celda y guardarlo de vuelta. Pásalo por un servicio las veces suficientes y de todos modos llega una queja. Las macros desaparecieron, o los tipos de cambio vinculados ahora muestran #REF!, y el equipo de operaciones está convencido de que tu código los eliminó. No eliminó nada. Lo que suele haber ocurrido es que un libro de trabajo habilitado para macros salió con un nombre .xlsx simple, y Excel obedeció las reglas de tipo de contenido de ECMA-376: un paquete cuyo tipo de contenido no declara VBA no puede cargar un proyecto VBA, sin importar que los bytes estén justo ahí. El archivo no se rompió. Se renombró a un estado en el que Excel está obligado a ignorar parte de él

Las macros y los vínculos a libros de trabajo externos son las dos cosas que la automatización pierde con mayor regularidad, por la misma razón de fondo. Ambos viven fuera de la cuadrícula de celdas que el código de edición realmente toca, así que el código que razona en términos de filas y columnas los descartará sin emitir jamás una eliminación. HotXLS es una biblioteca nativa para Delphi y C++Builder que lee y escribe XLS y XLSX sin Excel instalado, y trata ambos activos como cargas útiles que transporta deliberadamente y no como datos que copia por casualidad. Lo que sigue es lo que cada uno necesita de tu ruta de guardado, y dónde terminan las garantías

Por qué estos dos activos se comportan distinto ante una reescritura

Un proyecto VBA es un único binario opaco. En un paquete OOXML es el archivo vbaProject.bin; en un archivo BIFF heredado es un almacenamiento OLE. Hay exactamente dos formas de perderlo: el escritor nunca lo copia a la salida, o la salida recibe un tipo de archivo que lo prohíbe. Cualquiera de los dos fallos es total y silencioso. El proyecto está presente o no lo está

Un vínculo externo no es un blob en absoluto. Es un pequeño grafo de relaciones: una ruta o URL de destino que apunta a otro libro de trabajo, la lista de nombres de hoja que ese destino expone, y una caché opcional de los valores vistos por última vez en esas hojas para que Excel pueda mostrar algo cuando el destino está fuera de línea. Esas tres partes tienen distintos ciclos de vida ante una reescritura, y una biblioteca puede preservar fielmente algunas mientras descarta otras en silencio. Esa asimetría es la parte sobre la que vale la pena ser preciso, porque nada en el código de edición de celdas la hará visible

Diagrama comparativo de un blob de proyecto VBA frente a las tres partes de un vínculo a libro externo que HotXLS transporta a través de una reescritura en Delphi
Un proyecto VBA sobrevive a una reescritura como carga binaria de todo o nada, mientras que un vínculo externo es un pequeño grafo cuyo destino, nombres de hoja y valores en caché pueden preservarse o descartarse de forma independiente

Transportar un proyecto VBA a través de una reescritura XLSX

Del lado XLSX, TXLSXWorkbook conserva la carga útil de macros de forma literal. La propiedad VbaProject contiene los bytes crudos de vbaProject.bin dentro de un AnsiString, y una cadena vacía es la forma en que el modelo indica que no hay macros. A su alrededor hay tres operaciones: HasVbaProject responde si hay un proyecto presente, ClearVbaProject lo elimina a propósito, y LoadVbaProjectFromFile inyecta uno extraído de una plantilla. Esa última llamada vale más de lo que parece. Permite que los libros de trabajo generados incorporen un proyecto de macros estándar sin arrastrar un archivo de plantilla completo por el pipeline

Diagrama de flujo de una llamada de guardado en Delphi donde la extensión .xlsm selecciona el tipo de contenido habilitado para macros y .xlsx hace que Excel rechace las macros en silencio
HotXLS transporta los bytes crudos de vbaProject.bin a través del guardado, y la extensión .xlsm es lo que selecciona el tipo de contenido habilitado para macros que Excel requiere
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Data');
    Sheet.Cells[1, 1].Value := 'Refreshed ' + DateTimeToStr(Now);

    Book.LoadVbaProjectFromFile('macros\vbaProject.bin');
    if not Book.HasVbaProject then
      raise Exception.Create('VBA payload failed to load');

    // La extensión .xlsm no es cosmética: selecciona el
    // tipo de contenido habilitado para macros dentro del paquete.
    Book.SaveAs('monthly-report.xlsm');
  finally
    Book.Free;
  end;
end;

La línea de guardado es donde gira todo el problema. Un libro de trabajo que contiene un proyecto VBA tiene que escribirse con semántica habilitada para macros, y HotXLS la aplica cuando el nombre de destino termina en .xlsm. Pásale .xlsx en su lugar y Excel rechaza las macros, aunque los bytes estén físicamente presentes en el paquete y se deserializarían sin problema. La extensión no es decoración; selecciona el tipo de contenido que le dice a Excel que un proyecto VBA tiene permitido existir. La mayoría de las veces solo necesitas transportar la carga útil. Cuando necesitas leer dentro de ella, por ejemplo para listar los nombres de módulo en un informe de auditoría, ParsedVBAProject expone un modelo de módulos analizado mientras VbaProject permanece como los bytes originales sin tocar

Reutilizar macros de libros de trabajo XLS heredados

La fachada BIFF refleja ese conjunto de herramientas con un paso adicional. HasVBAProject sondea un archivo cargado, SaveVBAProjectToFile escribe el almacenamiento del proyecto en disco, y LoadVBAProjectFromFile lo lee de vuelta en otro libro de trabajo. El desvío a través de un archivo hace directa una tarea común de modernización: extraer las macros de un modelo de la era 2003 y plantarlas en una salida XLS recién generada, sin necesidad de la plantilla original en tiempo de ejecución

var
  Src, Dst: IXLSWorkbook;   // referencias de interfaz: sin Free manual
begin
  Src := TXLSWorkbook.Create;
  if Src.Open('legacy-model.xls') <= 0 then
    raise Exception.Create('Cannot open legacy model');
  if Src.HasVBAProject then
    Src.SaveVBAProjectToFile('extracted-vba.bin');

  Dst := TXLSWorkbook.Create;
  Dst.Sheets.Add.Name := 'Report2026';
  Dst.LoadVBAProjectFromFile('extracted-vba.bin');
  Dst.SaveAs('report-with-macros.xls');
end;

El modelo de memoria es la trampa aquí, y funciona al revés que la clase XLSX. TXLSWorkbook se sostiene a través de la interfaz con conteo de referencias IXLSWorkbook, así que nunca lo liberas a mano; el TXLSXWorkbook de XLSX es un objeto común que debes envolver en try..finally y liberar. Mezcla las dos convenciones en una sola unidad y siguen los fallos por doble liberación. Un límite más que vale la pena respetar: mantén la extracción y la inyección dentro de un solo formato de archivo. El almacenamiento de proyecto BIFF y el vbaProject.bin de OOXML son primos, no el mismo contenedor, y un pipeline que deba emitir macros en ambos formatos debería mantener una plantilla de macros separada para cada uno

Vínculos externos: el mapa sobrevive, los valores en caché no

Para libros de trabajo XLSX, HotXLS expone los vínculos externos a través de la colección ExternalLinks. Cada TXLSXExternalLink lleva un Target, la ruta o URL del libro de trabajo remoto, más una lista SheetNames que nombra las hojas a las que hace referencia. Ambos sobreviven intactos a un ciclo de apertura y guardado, y también puedes construir un vínculo desde cero:

var
  Link: TXLSXExternalLink;
begin
  Link := Book.ExternalLinks.Add('\\fileserver\finance\fx-rates-2026.xlsx');
  Link.SheetNames.Add('FX');

  if Book.ExternalLinks.Count > 0 then
    Writeln(Format('%d external link(s): delivery requires reachable targets',
      [Book.ExternalLinks.Count]));
end;

El límite está un nivel más profundo que la lista de destinos. HotXLS conserva de ida y vuelta el mapa del vínculo, es decir, el destino y los nombres de hoja, pero no analiza ni reescribe los valores de celda en caché que OOXML guarda en el elemento sheetDataSet del vínculo. Esa caché es lo que permite a Excel mostrar un último número conocido cuando el archivo de origen está fuera de línea, y un libro de trabajo generado se envía sin ella. La consecuencia recae en el destinatario, no en ti. Abre un archivo así donde el destino sea inalcanzable, una laptop fuera de la VPN o un recurso compartido que fue renombrado, y las fórmulas que dependen del vínculo se resuelven a #REF! o se quedan detenidas tras un aviso de actualización. De esto se desprenden dos reglas. No prometas que un libro de trabajo generado mostrará sus valores vinculados externamente sin conexión. Y lee un ExternalLinks.Count distinto de cero como una condición previa de entrega y no como una función: cada destino tiene que ser alcanzable desde donde el archivo realmente se abrirá

Diagrama de qué partes de un vínculo a libro externo de HotXLS sobreviven a una reescritura y qué ocurre cuando los valores en caché detrás de sheetDataSet están ausentes sin conexión
HotXLS conserva de ida y vuelta el destino del vínculo y sus nombres de hoja, pero los valores de celda en caché detrás de sheetDataSet no se transportan al archivo generado

Lo que el lector XLS preserva byte por byte

Para las estructuras que no modela, el lado BIFF tiene una respuesta distinta: dejarlas exactamente como las encontró. Las cachés y vistas de tablas dinámicas (la familia de registros SX*), las definiciones de QueryTable, las conexiones de datos externas, las vistas personalizadas, las imágenes de encabezado y los registros de tema pasan todos por un ciclo de apertura y guardado como bloques de registro crudos, sin analizar y sin modificar. Las referencias externas en sí hacen el viaje de ida y vuelta a través de los registros subyacentes EXTERNSHEET y SupBook. No hay una API tipada de creación para ellas del lado XLS, pero un vínculo existente sobrevive intacto a la edición

La preservación byte por byte es una garantía genuina con un filo cortante. Como nada lee una estructura preservada, tus ediciones no pueden corromperla. Por la misma razón, nada la actualiza tampoco. Inserta filas a través de una región a la que apunta una caché de tabla dinámica o una tabla de consulta preservadas, y la estructura mantiene sus coordenadas originales mientras los datos debajo se desplazan. El archivo sigue siendo XML o BIFF válido; el significado se ha desalineado silenciosamente, y no se dispara ningún error que te lo diga. El diseño defendible es mantener las ediciones generadas en hojas que no contengan estructuras preservadas, que es la misma disciplina que protege las hojas bloqueadas y configuradas para impresión en nuestro artículo sobre protección de hojas de cálculo y configuración de página

Verificar el archivo que realmente escribiste

Ambos modos de fallo son silenciosos en el momento de escribir, así que la aserción que importa se hace reabriendo la salida en lugar de confiar en el código que la produjo. Tres comprobaciones cubren casi todo. Reabre el archivo y confirma que HasVbaProject siga devolviendo true siempre que se esperaban macros, lo que detecta una carga útil descartada y una extensión incorrecta en una sola prueba. Lee ExternalLinks.Count y compáralo con el conteo previo a la reescritura. Luego abre el archivo una vez en Excel con las macros deshabilitadas, porque la validación de tipo de contenido de Excel es más estricta que la de cualquier biblioteca, y Excel es el programa por el que tus clientes juzgarán el archivo

Nada de eso requiere un análisis completo de entrada. Cuando los libros de trabajo llegan en volumen y solo necesitas clasificar cuáles llevan contenido regulado, el sondeo ligero de nuestro artículo sobre listado de hojas e inspección ligera de libros de trabajo te permite encaminar los archivos con macros y con vínculos a un pipeline más estricto antes de que se ejecute la primera reescritura

Algunas preguntas surgen con la frecuencia suficiente como para responderlas directamente. HotXLS nunca ejecuta las macros que preserva: no hay ningún runtime de VBA en la biblioteca, solo la maquinaria para almacenar, copiar, extraer e inyectar el proyecto como datos. En un servidor esa es una propiedad de seguridad que vale la pena declarar, ya que una macro hostil que pasa por el pipeline permanece inerte hasta que un Excel de escritorio abre el archivo y un usuario habilita el contenido. Convertir un .xlsm a .xlsx y conservar las macros no es posible, y esa es la regla del formato y no una limitación de la biblioteca: el tipo de contenido .xlsx declara un libro de trabajo sin macros, así que los únicos resultados honestos son permanecer en .xlsm o llamar a ClearVbaProject y enviar un archivo que genuinamente no tiene ninguna. El renombrado silencioso es la única opción que no satisface a nadie. Y cuando las celdas vinculadas muestran #REF! tras una reescritura, la causa es la caché de valores faltante comentada arriba: el archivo nuevo lleva el destino pero no los números en caché, así que Excel debe resolver el origen al abrir, y una ruta inalcanzable o relativa al entorno lo impide. O garantizas que el destino sea alcanzable, o escribes los valores calculados en las celdas antes de la entrega y eliminas por completo la dependencia

Editar los libros de trabajo de otras personas es, en su mayor parte, el trabajo de preservar cosas que no escribiste y que no entiendes del todo. Las facilidades de ida y vuelta de VBA y vínculos externos descritas aquí se incluyen con el componente HotXLS para Delphi para Delphi y C++Builder, junto con las propiedades de auditoría que te permiten detectar contenido regulado en el momento en que llega un archivo