Artículo técnico

Registros Selection BIFF8 y scroll de paneles en HotXLS

HotXLS guarda las selecciones de hoja y las posiciones de scroll por panel a través de una única API consciente de paneles, tanto en TXLSWorksheet como en TXLSXWorksheet: SelectAreas, GetSelectedAreas, ScrollWindow y TryGetWindowScroll. Para archivos .xls clásicos, HotXLS escribe registros Selection de BIFF8 (0x001D) de 1369 áreas como máximo cada uno, convierte los nombres lógicos de panel en los bytes de panel que define el formato y deja cada eje de scroll en el registro Window2 o Pane donde Excel lo espera

El problema suele aparecer en una herramienta de conciliación o de auditoría. La herramienta abre un export del libro mayor, encuentra cada celda que discrepa del sistema fuente y guarda el libro con esas celdas ya seleccionadas bajo una fila de encabezado congelada, para que el revisor aterrice en las diferencias en lugar de andar buscándolas con el scroll. Con cuarenta diferencias funciona bien. El archivo de cierre de mes trae 3,000, y un único registro Selection con 3,000 áreas no puede existir: su cuerpo necesitaría 18,009 bytes, más del doble de lo que un registro BIFF8 puede cargar. La posición de scroll tiene una trampa parecida. En una hoja con paneles congelados, «dónde estaba mirando el usuario» son cuatro paneles compartiendo dos posiciones de fila y dos de columna, no una sola coordenada

¿Por qué una selección grande necesita más de un registro Selection?

Una selección grande necesita varios registros porque el cuerpo de un registro BIFF8 está limitado a 8224 bytes y cada área seleccionada cuesta seis bytes fijos. [MS-XLS] §2.4.248 estructura el registro Selection como una parte fija de 9 bytes (el byte de panel, rwAct y colAct para la celda activa, irefAct para el área activa y cref para el conteo de áreas) seguida de cref estructuras RefU, cada una con dos filas de 16 bits y dos columnas de 8 bits. El conteo máximo que cabe es (8224 − 9) / 6 redondeado hacia abajo, o sea 1369, y eso produce un cuerpo de 8223 bytes, un byte bajo el límite. TXLSWorksheet.StoreSelectionGroup usa esa constante como MaxAreasPerRecord y escribe un grupo mayor como registros Selection consecutivos del mismo panel, 1369 áreas a la vez

El detalle que muerde es irefAct. Cada bloque repite la misma fila activa, columna activa e índice de área activa, y irefAct indexa la secuencia agregada de todos los bloques, no las áreas dentro del registro que lo lleva. Una selección con un área más allá del límite lo hace concreto: 1370 áreas con la última activa se vuelven dos registros, el primero con cref 1369 y el segundo con cref 1, y ambos cargan irefAct 1369. Ese valor es mayor que el conteo de áreas del segundo registro. Un lector que verifica irefAct contra cref en cada registro rechaza un archivo válido, y un lector que reemplaza su estado con cada registro pierde las primeras 1369 áreas. El lector de HotXLS agrega los registros consecutivos del mismo panel en un solo grupo, exige que cada bloque concuerde en la celda activa y el índice, y corre la verificación de rango solo en el registro EOF de la hoja, cuando la secuencia completa ya es conocida. La sobrecarga de SelectAreas que recibe primero el panel, por tanto, no tiene techo de 1369 áreas. Valida cada referencia A1 y el índice activo antes de tomar el lock de escritura de la hoja, y devuelve False con la selección previa intacta si algo está mal formado

Por qué HotXLS escribe una selección grande de hoja como varios registros Selection BIFF8: el tope de cuerpo de 8,224 bytes cabe 9 bytes fijos más 1369 áreas RefU de seis bytes, así que 3,000 áreas se vuelven tres registros del mismo panel de 1369, 1369 y 262, y irefAct indexa la secuencia agregada, de modo que 1370 áreas con la última activa dan a ambos registros irefAct 1369
Cada bloque repite la misma celda activa e índice, el lector de HotXLS agrega los registros consecutivos del mismo panel en un solo grupo, y la verificación de rango corre solo en el registro EOF cuando la secuencia completa ya es conocida
var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Diffs: TXLSSelectedAreas;
  I: Integer;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add;
    Sheet.FreezePanes(1, 1);           // el encabezado y la columna A se quedan quietos

    SetLength(Diffs, 3000);
    for I := 0 to High(Diffs) do
      Diffs[I] := Format('C%d', [I + 2]);

    // Congelar resetea la selección guardada, así que seleccione después de congelar.
    // 3000 áreas se guardan como tres registros Selection: 1369 + 1369 + 262
    if not Sheet.SelectAreas(xlspBottomRight, Diffs, 0) then
      raise Exception.Create('Selection rejected');

    Book.SaveAs('reconciliation.xls');
  finally
    Book.Free;
  end;
end;

¿Qué byte de panel usa un registro Selection?

Un registro Selection identifica su panel por el código numérico que define el formato: 0 para inferior derecha, 1 para superior derecha, 2 para inferior izquierda y 3 para superior izquierda. La enumeración pública TXLSPanePosition se declara en orden de lectura, xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight, de modo que Ord(xlspTopLeft) es 0, que es el panel inferior derecho en el archivo. Hacer un cast de la enumeración directo al byte de panel escribiría toda selección superior izquierda sobre el panel inferior derecho sin ningún error. Cada punto de entrada de HotXLS consciente de paneles convierte la enumeración por una sentencia case explícita, así que quien llama nunca lidia con los códigos numéricos. La existencia del panel también se verifica: el panel superior derecho existe solo con una división vertical, el inferior izquierdo solo con una horizontal, y el inferior derecho solo con ambas. Para un panel que la geometría actual de división o congelado no tiene, SelectAreas devuelve False, y GetSelectedAreas devuelve un arreglo vacío con ActiveAreaIndex en -1, sin crear un panel, un objeto de selección ni una celda en el libro

Cómo HotXLS mapea TXLSPanePosition al byte de panel de Selection BIFF8: la enumeración se declara en orden de lectura así que Ord(xlspTopLeft) es 0, mientras el archivo define 0 para inferior derecha, 1 para superior derecha, 2 para inferior izquierda y 3 para superior izquierda, así que cada punto de entrada consciente de paneles convierte por una sentencia case explícita
Hacer un cast de la enumeración directo al byte de panel escribiría toda selección superior izquierda sobre el panel inferior derecho, así que HotXLS también verifica la existencia del panel contra la geometría actual de división o congelado antes de escribir

¿Dónde vive la posición de scroll de cada panel?

La posición de scroll de cada panel está repartida entre dos registros, porque cuatro paneles comparten solo dos posiciones de fila y dos de columna. En un libro clásico, la primera fila visible de los paneles superiores y la primera columna visible de los izquierdos son Window2.rwTop y Window2.colLeft, mientras que la fila de los paneles inferiores y la columna de los derechos son Pane.rwTop y Pane.colLeft. ScrollWindow(xlspTopRight, R, C) escribe entonces Window2.rwTop y Pane.colLeft, y fijar la columna del panel superior derecho también mueve el inferior derecho, igual que los dos comparten una barra de scroll horizontal en Excel. Los métodos públicos usan números de fila y columna desde 1. Un panel inexistente devuelve False y pone ambas salidas de la consulta en cero, y una coordenada fuera de rango se rechaza antes de que cambie cualquier eje. Nada de esto depende de cómo un visor pinta la cuadrícula. Un control de renderizado mantiene su propio TopRow y LeftCol, como describe el artículo sobre renderizar libros en una cuadrícula VCL propia, y eso es estado en runtime, no lo que se guarda

Dónde vive cada eje de scroll de panel en HotXLS: cuatro paneles comparten dos posiciones de fila y dos de columna, así que la fila superior y la columna izquierda son Window2.rwTop y Window2.colLeft, mientras la fila inferior y la columna derecha son Pane.rwTop y Pane.colLeft, y ScrollWindow(xlspTopRight, 1, 6) escribe un campo Window2 más un campo Pane para que inferior derecho lo siga
XLSX reparte los mismos datos entre los atributos sheetView y pane topLeftCell, y colapsar las dos capas en una es exactamente cómo una posición de scroll superior o izquierda desaparece en silencio al cargar

XLSX reparte los mismos datos entre dos elementos: sheetView/@topLeftCell (ECMA-376 Part 1, §18.3.1.87) para la ventana en su conjunto y el hijo pane/@topLeftCell (§18.3.1.66) para el lado inferior derecho de una división. Ambos atributos pueden estar presentes a la vez. HotXLS lee primero el atributo externo hacia los campos de nivel de ventana, deja que el hijo pane sobrescriba solo los campos de nivel de panel, y escribe ambos de vuelta por separado. Colapsar las dos capas en una es exactamente cómo una posición de scroll superior o izquierda desaparece en silencio al cargar. Las copias de hojas cargan ambas capas en ambos motores. Los puntos de entrada más viejos conservan su comportamiento original: las propiedades clásicas ScrollRow y ScrollColumn, y el SetPaneScroll y GetPaneScroll de XLSX con base cero. La geometría de congelado y división en sí se configura con los ajustes de hoja cubiertos en protección de hoja, configuración de página e impresión

var
  Row, Col: Integer;
begin
  Sheet.FreezePanes(1, 1);

  // Inferior derecha: eje de fila inferior (Pane.rwTop) y eje de columna derecha (Pane.colLeft)
  Sheet.ScrollWindow(xlspBottomRight, 500, 3);

  // Superior derecha comparte el eje de columna derecha, así que esto también mueve inferior derecha a la columna 6
  Sheet.ScrollWindow(xlspTopRight, 1, 6);

  if Sheet.TryGetWindowScroll(xlspBottomRight, Row, Col) then
    Memo1.Lines.Add(Format('Bottom-right starts at row %d, column %d', [Row, Col]));
    // Inferior derecha arranca en la fila 500, columna 6
end;

¿Qué pasa cuando un registro Selection está corrupto?

Cuando un registro Selection está corrupto, HotXLS lo conserva como bytes opacos, reporta el código de diagnóstico 1304 (xlsDiagnosticSelectionRecordInvalid) y reescribe el cuerpo original byte a byte al guardar. Antes de que un registro se una al grupo de su panel, el lector lo verifica en orden. El byte de panel debe ser 3 o menos. Los registros de un panel deben ser contiguos en el stream. Los 9 bytes fijos deben estar presentes. cref debe estar entre 1 y 1369, y el cuerpo debe medir exactamente 9 + cref × 6 bytes. Cada bloque del grupo debe concordar en la celda activa y irefAct, irefAct no debe tener el bit de signo encendido, la columna activa debe estar en la cuadrícula, y ninguna área puede tener límites invertidos. Los problemas de un registro físico individual se reportan una vez por registro. Las contradicciones que solo aparecen tras la agregación, como un irefAct apuntando más allá del conteo total de áreas o una celda activa fuera del área indexada, se reportan una vez por grupo al llegar al EOF. Un grupo inválido queda invisible para la API tipada: GetSelectedAreas devuelve un arreglo vacío con índice -1 para ese panel, mientras los demás paneles siguen funcionando

var
  I: Integer;
  D: TXLSDiagnostic;
begin
  if Book.Open('supplier-upload.xls') <> 1 then
    Exit;
  for I := 0 to Book.Diagnostics.Count - 1 do
  begin
    D := Book.Diagnostics[I];
    if D.Code = xlsDiagnosticSelectionRecordInvalid then
      Log.Add(Format('%s: record $%.4x kept opaque (%s)',
        [D.SheetName, D.RecordId, D.Message]));
  end;
end;

¿Cómo sobreviven las selecciones a inserciones de filas y columnas?

Las selecciones sobreviven a las ediciones estructurales porque insertar o eliminar filas o columnas completas remapea cada grupo de panel representado, en ambos motores el clásico y el XLSX, a través de un remapeador compartido. Las áreas sobrevivientes conservan su orden y el área activa conserva su identidad. Si el área activa se elimina, la primera sucesora sobreviviente se vuelve activa, y si nada la sigue, la última predecesora sobreviviente. Si se eliminan todas las áreas, el grupo colapsa a una celda en el borde de la eliminación, y una celda activa que ya no cae dentro del área elegida se mueve a la esquina superior izquierda de esa área, así que el índice y la coordenada nunca se contradicen. Los límites son deliberados. Los grupos clásicos inválidos los salta el remapeador en lugar de reescribirlos en una selección inventada, así que sus bytes originales siguen haciendo round-trip. Editar un panel reemplaza solo los registros de ese panel y deja los demás idénticos byte a byte. ODS no recibe estado de selección de paneles para nada, porque ODF no tiene una estructura de vista de hoja equivalente que lo cargue

Si su aplicación escribe archivos .xls que los usuarios abren y necesitan navegar, ya sea para revisar celdas marcadas, retomar donde se quedaron o compartir un dashboard congelado, la API de selección y scroll consciente de paneles forma parte del componente de hojas de cálculo HotXLS para Delphi, y funciona igual para XLS y XLSX