Artículo técnico

Gráficos, imágenes y objetos de dibujo de HotXLS en Delphi

El ticket de soporte decía que faltaba el gráfico. Un servicio Delphi generaba un informe mensual .xls, creaba una hoja de gráfico para la tendencia de ingresos y luego intentaba escribir una leyenda de datos en esa misma hoja con llamadas de celda. No se produjo ninguna excepción durante la generación, pero Excel descartó en silencio el flujo de dibujo inconsistente al abrir el archivo. La causa raíz es un detalle del formato que es fácil pasar por alto: en BIFF8, una hoja de gráfico es un subflujo separado, no una hoja de cálculo con un gráfico flotando sobre ella, y las operaciones de nivel de celda contra esa hoja no son compatibles. HotXLS, una biblioteca nativa de Object Pascal que lee y escribe XLS y XLSX sin automatización de Excel, expone ambos modelos de dibujo, pero son modelos realmente diferentes, y la mayoría de los incidentes de producción con gráficos e imágenes vienen de aplicar las reglas de un formato al otro.

Dos formatos de archivo, dos modelos de dibujo

Antes de escribir cualquier código de gráficos, definan qué contenedor van a producir, porque los tipos de objeto disponibles son distintos:

  • XLS (BIFF8): los gráficos viven en hojas de gráfico dedicadas creadas mediante AddChartSheet en la colección Sheets. Las imágenes, cuadros de texto, rectángulos, óvalos y líneas son formas OfficeArt administradas mediante la colección Shapes de la hoja de cálculo. No hay API para incrustar un gráfico dentro de una cuadrícula normal de hoja de cálculo.
  • XLSX (OOXML): los gráficos pueden incrustarse directamente en una hoja de cálculo con TXLSXWorksheet.AddChart, anclarse a un rectángulo de celdas o colocarse en una hoja de gráfico dedicada con TXLSXWorkbook.AddChartSheet. Las imágenes entran con AddImage o AddImageFromFile, y las etiquetas flotantes con AddTextBox.

Si el requisito es "una hoja de tablero con el gráfico junto a los números", el entregable es XLSX. Intentar satisfacerlo en .xls obliga a usar una hoja de gráfico separada, lo que cambia la navegación para el usuario y cambia el código: la hoja devuelta por AddChartSheet del lado XLS debe tratarse como solo gráfico y nunca escribirse con Cells.Item. Esa única regla habría evitado el incidente anterior.

Incrustar un gráfico en una hoja XLSX

La ruta XLSX es la más flexible, así que vale la pena aclarar los dos sistemas de coordenadas. El rectángulo de anclaje pasado a AddChart se expresa en filas y columnas de la hoja y define dónde se ubica el marco del gráfico; los datos de la serie se expresan como referencias A1 absolutas que incluyen el nombre de la hoja. Son independientes: mover el marco del gráfico nunca cambia lo que grafica.

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Chart: TXLSXChart;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Sales');
    Sheet.Cells[1, 1].Value := 'Region';
    Sheet.Cells[1, 2].Value := 'Revenue';
    Sheet.Cells[2, 1].Value := 'East';
    Sheet.Cells[2, 2].Value := 1184350;
    Sheet.Cells[3, 1].Value := 'Central';
    Sheet.Cells[3, 2].Value := 902210;
    Sheet.Cells[4, 1].Value := 'West';
    Sheet.Cells[4, 2].Value := 1010675;

    // Frame anchored to rows 6..22, columns 1..8
    Chart := Sheet.AddChart(xlsxChartColumn, 'Revenue by Region', 6, 1, 22, 8);
    Chart.AddSeries('Revenue', 'Sales!$A$2:$A$4', 'Sales!$B$2:$B$4');
    Chart.ValueAxisTitle := 'USD';

    Sheet.AddImageFromFile(1, 5, 'logo.png');
    Book.SaveAs('dashboard.xlsx');
  finally
    Book.Free;
  end;
end;

La línea que falla con más frecuencia es AddSeries. Los argumentos de categorías y valores son cadenas de rango literales, de modo que no crecen cuando se agregan más filas más adelante en la ejecución. Escriban primero los datos, registren el número final de filas y solo entonces construyan la referencia de la serie a partir de ese conteo. Para gráficos de dispersión y burbuja, la misma llamada cambia de semántica: el rango de categorías aporta los valores X y el rango de valores aporta Y, con los tamaños de burbuja configurados aparte mediante BubbleSizeRange en el TXLSXChartSeries devuelto.

Los valores disponibles de TXLSXChartType cubren gráficos de columnas, barras, líneas, pastel, áreas, dona, dispersión, burbuja y radar. Cuando el entregable es un gráfico de página completa, Book.AddChartSheet crea una hoja cuya propiedad IsChartSheet es true: el equivalente XLSX de la hoja de gráfico heredada, con la misma expectativa de "sin contenido de cuadrícula".

Las imágenes son bytes, y los tamaños son EMU

El error de imagen más común en revisión de código es pasar una ruta de archivo al overload equivocado. AddImage(ARow, ACol, AData, AFormat) recibe en su parámetro AData los bytes de la imagen ya codificada: los datos PNG, JPEG, GIF o BMP sin procesar. Si le entregan una cadena de ruta, habrán insertado una "imagen" de cuarenta bytes que ningún visor puede decodificar. Cuando la fuente es un archivo en disco, usen AddImageFromFile y dejen que la biblioteca lo cargue y clasifique.

El siguiente tropiezo suele ser el tamaño. DrawingML mide en English Metric Units: 914400 EMU por pulgada, lo que equivale a 9525 EMU por píxel a 96 DPI. El objeto TXLSXImage expone WidthEMU y HeightEMU, así que un logotipo que debe mostrarse a 180 por 60 píxeles necesita 1714500 por 571500 EMU. Hagan esa aritmética una vez en una constante auxiliar en vez de repartir números mágicos, y recuerden que la fila y la columna de anclaje son 1-based como el resto de la API de celdas.

Hojas de gráfico y formas en archivos XLS heredados

Del lado BIFF8, el overload más completo de AddChartSheet acepta el tipo de gráfico, los títulos y un arreglo abierto de registros TXLSChartSeriesInfo, cada uno con nombre, categorías y valores como cadenas de rango. Las formas se agregan en la propia hoja de datos:

var
  Book: IXLSWorkbook;
  Data, Trend: IXLSWorksheet;
  Series: array[0..0] of TXLSChartSeriesInfo;
begin
  Book := TXLSWorkbook.Create;   // interface-counted: do not Free
  Data := Book.Sheets.Add;
  Data.Name := 'Data';
  Data.Cells.Item[1, 1].Value := 'Month';
  Data.Cells.Item[1, 2].Value := 'Units';
  Data.Cells.Item[2, 1].Value := 'Apr';
  Data.Cells.Item[2, 2].Value := 1530;
  Data.Cells.Item[3, 1].Value := 'May';
  Data.Cells.Item[3, 2].Value := 1721;

  Series[0].Name := 'Units';
  Series[0].Categories := 'Data!$A$2:$A$3';
  Series[0].Values := 'Data!$B$2:$B$3';
  Trend := Book.Sheets.AddChartSheet('Trend', xlsChartTypeLine,
    'Units sold', 'Month', 'Units', Series);
  // Trend is a chart substream: never call cell methods on it

  Data.Shapes.AddTextBox('Source: ERP nightly export', 6, 1, 8, 4);
  Data.Shapes.AddPicture('approved-stamp.bmp');
  Book.SaveAs('trend.xls');
end;

Aquí importan dos detalles de vida útil. TXLSWorkbook se mantiene mediante la interfaz IXLSWorkbook y usa conteo de referencias, por lo que liberarlo manualmente provoca una doble liberación; esto es lo opuesto de TXLSXWorkbook, que sí debe liberarse en un bloque try..finally. Además, los auxiliares de forma, AddRectangle, AddOval, AddLine, más DeleteInRange para limpiar una región de dibujos, se anclan todos mediante pares de fila y columna, así que una plantilla que inserta filas por encima de las formas las desplazará junto con la cuadrícula.

TXLSPicture también admite TransparentColor para enmascarar el color de fondo de los bitmaps, que es la ruta práctica para poner un sello no rectangular sobre el contenido de la cuadrícula en un formato anterior al soporte de alfa PNG en el renderizado BIFF.

Los colores de tema no sobreviven una ida y vuelta por BIFF8

Los rellenos de dibujo OOXML pueden referirse a una ranura de color de tema, así que recolorear todo un documento cambiando el tema es barato en .xlsx. Los registros de dibujo BIFF8 no tienen una ranura equivalente. Cuando HotXLS aplica un color de tema a un dibujo XLS, resuelve y almacena el valor RGB final; después de guardar y volver a abrir el archivo, el índice de tema original simplemente ya no está disponible para leerse. Si su producto cambia la marca de documentos generados, por ejemplo una herramienta de reportes white-label, mantengan el mapa de tema a RGB en la configuración de la aplicación y vuelvan a aplicarlo durante la generación, en lugar de esperar recuperarlo desde un .xls guardado.

Un trade-off relacionado aparece en el extremo de rendimiento: la fachada XLS puede omitir el análisis de toda la capa de dibujo cuando solo necesitan datos de celdas de archivos heredados grandes, lo que acelera bastante las lecturas masivas, pero un libro cargado así nunca debe guardarse, porque el flujo OfficeArt omitido ya no está. La técnica pertenece a trabajos analíticos de solo lectura, cubiertos con más detalle en nuestras notas sobre rendimiento de libros grandes en HotXLS.

Mantener los anclajes estables mientras cambia la cuadrícula

Los reportes rara vez conservan el tamaño con el que fueron generados. Las operaciones estructurales de la fachada XLSX, como InsertRows, DeleteRows y sus equivalentes de columna, desplazan las capas dependientes junto con las celdas: regiones combinadas, hipervínculos, comentarios, paneles congelados, rangos de filtro, formatos condicionales, validaciones, tablas, nombres definidos y, de forma importante para este tema, anclajes de imagen y gráfico se mueven juntos. Un logotipo anclado en la fila 1 permanece arriba cuando se insertan diez filas debajo; un marco de gráfico anclado bajo el bloque de datos se desliza hacia abajo cuando el bloque crece. Lo que no se reescribe es cualquier cosa que ustedes hayan guardado como cadena literal antes de la inserción, así que el orden seguro para llenar una plantilla es: escribir y remodelar los datos primero, luego crear gráficos y colocar imágenes como pasada final, derivando cada cadena de rango de los conteos de filas posteriores a la inserción.

Dos herramientas de ubicación más pequeñas completan el kit. Del lado XLS, TXLSTextBox.SetArea reposiciona un cuadro de texto o una forma automática sobre un nuevo rectángulo de celdas después del hecho, lo que es más barato que borrarlo y recrearlo cuando se mueve un bloque de pie. Y el overload bitmap de AddPicture acepta un TBitmap vivo con una bandera opcional de transparencia, de modo que los gráficos renderizados por su propio código de dibujo VCL, indicadores, tiras de minigráficos, cualquier cosa que los tipos de gráfico nativos no cubran, pueden sellarse en la hoja sin pasar por un archivo temporal.

FAQ: gráficos e imágenes en HotXLS

¿Puedo incrustar un gráfico dentro de una hoja .xls normal? No. La API BIFF8 crea solo hojas de gráfico. Si el diseño requiere un gráfico flotando junto a los datos, generen .xlsx con TXLSXWorksheet.AddChart o reestructuren el entregable alrededor de una hoja de gráfico.

¿Por qué mi imagen insertada aparece como un icono roto en Excel? Casi siempre porque se pasó una ruta de archivo a AddImage, que espera bytes de imagen codificados. Cambien a AddImageFromFile o lean primero el archivo en la cadena de datos.

¿Cómo hago que una imagen tenga exactamente el tamaño del bitmap original? Multipliquen las dimensiones en píxeles por 9525 y asignen los resultados a WidthEMU y HeightEMU. Excel escala cualquier otra cosa según lo que implique el anclaje, que rara vez es lo que pretendía el diseñador.

Los gráficos y las imágenes suelen llegar como la capa final de un reporte estructurado, así que la base importa: la generación de reportes basada en plantillas cubre cómo llenar los datos que el gráfico referenciará, y las celdas combinadas y el control de diseño cubren cómo mantener estable la cuadrícula bajo sus anclajes. La documentación completa de clases y métodos está en la página de producto HotXLS Component.