Artículo técnico

Incrustar objetos de gráfico en hojas de cálculo con HotXLS

HotXLS puede colocar un gráfico directamente sobre una hoja de cálculo, anclado a un rango de celdas, en lugar de ponerlo en una hoja de gráfico dedicada. En términos BIFF8 eso significa escribir una forma de dibujo con un registro OBJ de tipo 5 y aparcar el substream de gráfico al final del stream de registros de la hoja, que es exactamente la disposición que Excel produce y exactamente donde el lector espera encontrarlo

La distinción importa a cualquiera que genere informes operativos. Una hoja de gráfico es un buen hogar para un único visual de portada. Un desglose regional mensual quiere el gráfico al lado de los números que resume, en la misma hoja, dimensionado al bloque de celdas al que pertenece, de modo que el lector haga scroll una vez en vez de cambiar de pestañas y perder el contexto

La lectura ya estaba, la escritura no

La asimetría merece nombre porque condiciona el trabajo. HotXLS ya podía leer gráficos incrustados: cuando el stream de registros de la hoja contiene un BOF marcado como substream de gráfico, el parser cambia de contexto, recoge los registros de gráfico y, en el EOF de cierre, los devuelve a la forma de dibujo que el registro OBJ introdujo. Esa ruta la había ejercitado cada libro de Excel que la biblioteca abrió jamás

Lo que faltaba era el lado de autoría, y la consecuencia útil es que el nuevo escritor tenía una especificación precisa que alcanzar: producir la disposición de bytes que el lector existente ya reengancha. No hay mejor criterio de aceptación para una función de formato binario que un lector escrito de forma independiente que usted no pudo cambiar

De qué está hecho un gráfico incrustado

Tres piezas tienen que concordar. La capa de dibujo aporta una forma de control anfitrión, la capa de objetos aporta un registro OBJ cuyos datos comunes de objeto declaran el tipo de objeto 5, y el stream de registros aporta el propio substream de gráfico. Los flags de opciones del registro OBJ son los que Excel escribe para un marco de gráfico: posicionado, bloqueado, línea automática y relleno automático, que es lo que hace que el gráfico incrustado se comporte como uno nativo cuando un usuario hace clic en él

HotXLS ancla un substream de gráfico BIFF8 a una hoja Delphi a través de tres piezas que concuerdan: la forma de control anfitrión de la capa de dibujo, el registro OBJ cuyos datos comunes de objeto declaran el tipo de objeto 5, y la cadena de registros de gráfico aparcada al final del stream de registros de la hoja, donde un BOF de gráfico cambia el contexto del parser y el EOF de cierre reengancha los registros
Tres capas llevan un gráfico incrustado: la forma de dibujo lo ancla, el registro OBJ lo tipa como anfitrión de gráfico, y el substream de gráfico al final del stream de la hoja suministra los registros que el lector reengancha

El ancla merece una nota porque es una fuente común de bugs de desvío de uno. La API de HotXLS toma números de fila y columna base uno, en línea con el resto de la biblioteca, y el ancla de cliente escrita en el fichero es base cero. La conversión ocurre dentro de AddChartObject, así que quienes llaman permanecen en el sistema de coordenadas que usan en todo lo demás, pero cualquiera que compare un volcado hexadecimal con su propia llamada necesita recordar en qué lado de esa frontera está leyendo

var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Series: array[0..1] of TXLSChartSeriesInfo;
begin
  Book := TXLSWorkbook.Create(nil);
  try
    Book.LoadFromFile('regional-sales.xls');
    Sheet := Book.Sheets[0];

    FillChar(Series, SizeOf(Series), 0);
    Series[0].Name := 'Actual';
    Series[0].Categories := 'Data!$A$2:$A$13';
    Series[0].Values := 'Data!$B$2:$B$13';
    Series[0].DataLabels.ShowValue := True;
    Series[0].HasDataLabels := True;

    Series[1].Name := 'Target';
    Series[1].Categories := 'Data!$A$2:$A$13';
    Series[1].Values := 'Data!$C$2:$C$13';
    Series[1].SecondaryAxis := True;

    // Anclado a E2:M20 en esta hoja, base uno
    Sheet.AddChartObject(xlsChartTypeColumn, 'Regional sales',
      'Month', 'Amount', Series, 2, 5, 20, 13);

    Book.SaveToFile('regional-sales-charted.xls');
  finally
    Book.Free;
  end;
end;

El FillChar sobre el array de series no es decoración. TXLSChartSeriesInfo lleva varios sub-registros opcionales, etiquetas de datos, estilo por serie, líneas de tendencia y barras de error, cada uno de ellos activado por un booleano, y un registro parcialmente inicializado en la pila entregará al emisor flags que nadie puso. Ponga el array a cero, y luego fije los campos que quiere

¿Qué referencias de serie acepta la ruta incrustada?

Rangos llanos de estilo A1 dentro del mismo libro, y esa restricción es deliberada más que un descuido. Cada referencia se resuelve contra la lista de hojas del libro y se convierte en el índice de referencia externa que los registros de gráfico necesitan. Un rango con nombre o una referencia a un libro externo recurre a un placeholder con expresión parseada de longitud cero, así que el gráfico se escribe limpiamente pero esa serie concreta no tiene fuente de datos hasta que la apunte a un rango

Aceptación de referencias de serie de HotXLS en la ruta de gráfico BIFF8 incrustado: los rangos de estilo A1 como Data!$B$2:$B$13 dentro del mismo libro se resuelven contra la lista de hojas en el índice de referencia externa que los registros de gráfico necesitan, mientras que los rangos con nombre y las referencias a libros externos recurren a un placeholder con expresión parseada de longitud cero, con ambos cubiertos por AddChartSheet
Solo los rangos llanos de estilo A1 dentro del mismo libro se compilan en referencias de serie de gráfico; todo lo demás se escribe limpiamente como placeholder hasta reorientarse, y la ruta completa vive en AddChartSheet

La razón es un intercambio de ingeniería directo. La ruta completa de compilación de referencias existe en la ruta de hoja de gráfico, envuelta dentro de la capa de colección de hojas, y extraerla limpiamente significaría duplicar cien líneas de lógica de resolución para un caso poco común en la práctica. Un gráfico incrustado casi siempre grafica celdas de su propia hoja o de una hoja de datos hermana. Las referencias con nombre y externas están cubiertas en la ruta de hoja de gráfico mediante AddChartSheet, así que nada está indisponible, solo se alcanza desde otro punto de entrada

Todo lo demás en el modelo de series funciona igual en ambas rutas. La vinculación de eje secundario, los estilos de línea, relleno y marcador por serie, las líneas de tendencia, las barras de error y las etiquetas de datos son parte de TXLSChartSeriesInfo y todos se emiten de la misma manera, así que una definición de gráfico puede moverse entre un objeto incrustado y una hoja de gráfico cambiando solo la llamada. La mecánica de grupos de ejes detrás del flag de eje secundario se cubre en los grupos de eje secundario en la escritura BIFF

¿Por qué el título del gráfico se leía como dos caracteres?

Porque se pasó un recuento de caracteres donde se esperaba un recuento de bytes, y las cadenas Unicode de BIFF facilitan escribir ese error y dificultan verlo. Una cadena Unicode corta de BIFF empieza con un recuento de caracteres y un byte de flags, y el byte de flags lleva el bit de byte alto que dice si el payload es de un byte por carácter o de dos. Lea un payload de 16 bits con el recuento de caracteres como si fuera una longitud en bytes y obtendrá exactamente la mitad de la cadena: una serie llamada Sales vuelve como Sa, y un título de gráfico se trunca igual porque títulos y etiquetas de serie comparten la ruta de decodificación

Lo que hace notable este defecto es que recrudeció tres veces en la misma familia de registros, una en nombres de líneas de tendencia, otra en nombres de gráficos dinámicos y otra en títulos de gráfico. Cada aparición parecía un bug fresco en una función nueva. Las tres eran el mismo multiply ausente. La regla que finalmente lo cerró es mecánica y debería aplicarse sin juicio: siempre que lea una de estas cadenas, consulte primero el flag de byte alto y multiplique el recuento de caracteres por el ancho del payload antes de tocar el buffer. Los detalles a nivel de registro están en decodificar recuentos de caracteres de XLUnicodeString y el flag de byte alto

// El gráfico incrustado comparte la capa de dibujo con imágenes y
// formas, así que un dibujo existente en la hoja se conserva.
// AddChartObject devuelve el índice del objeto creado
var
  ObjIndex: Integer;
begin
  ObjIndex := Sheet.AddChartObject(xlsChartTypeLine, 'Trend',
    'Week', 'Units', Series, 2, 8, 18, 16);
  if ObjIndex < 0 then
    raise Exception.Create('chart object was not created');
end;

Dónde encajan los gráficos incrustados frente a las alternativas

Existen tres rutas y responden a preguntas distintas. Un objeto de gráfico incrustado pertenece al lado de sus datos en una hoja de cálculo y es lo que la mayoría de los informes quieren. Una hoja de gráfico conviene a un único visual de presentación y le da la ruta completa de compilación de referencias. Preservar un gráfico existente de un fichero cargado, intacto, es la respuesta correcta cuando el libro vino de Excel con formato que nadie quiere que una biblioteca reinterpretara; ese comportamiento de paso directo se describe en el ChartML preservado y los gráficos combinados

Como el gráfico incrustado viaja sobre la capa de dibujo, coexiste con imágenes y formas en la misma hoja en lugar de reemplazarlas, y el modelo general de esa capa se cubre en gráficos, imágenes y dibujos en HotXLS. Las tres rutas se envían en el componente de hoja de cálculo Delphi HotXLS, así que la elección va de cómo debe verse el informe y no de qué puede expresar la biblioteca

El punto metodológico es el que merece conservarse. Cuando una función de formato binario tiene un lector existente, construya el escritor contra el lector y no contra su lectura de la especificación. El lector codifica años de contacto con ficheros que aplicaciones reales produjeron de verdad, incluidas las partes que la especificación enuncia con vaguedad, y un escritor que lo satisface es mucho más probable que también satisfaga a Excel