Artículo técnico

Records Selection de BIFF8 y scroll de paneles en HotXLS

HotXLS almacena las selecciones de hoja y las posiciones de scroll por panel mediante una única API consciente de paneles tanto en TXLSWorksheet como en TXLSXWorksheet: SelectAreas, GetSelectedAreas, ScrollWindow y TryGetWindowScroll. Para los archivos .xls clásicos, HotXLS escribe records 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 record 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 de origen, y guarda el workbook con esas celdas ya seleccionadas bajo una fila de cabecera inmovilizada, para que el revisor caiga directamente en las diferencias en vez de andar haciendo scroll hasta ellas. Con cuarenta diferencias funciona de perlas. El archivo de cierre de mes tiene 3.000, y un único record Selection que albergara 3.000 áreas no puede existir: su cuerpo necesitaría 18.009 bytes, más del doble de lo que un record BIFF8 puede cargar. La posición de scroll tiene una trampa parecida. En una hoja con paneles inmovilizados, "dónde estaba mirando el usuario" son cuatro paneles compartiendo dos posiciones de fila y dos de columna, no una coordenada

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

Una selección grande necesita varios records porque el cuerpo de un record BIFF8 está limitado a 8224 bytes y cada área seleccionada cuesta seis bytes fijos. [MS-XLS] §2.4.248 estructura el record Selection como una parte fija de 9 bytes (el byte de panel, rwAct y colAct de la celda activa, irefAct del área activa y cref del 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 por debajo del límite. TXLSWorksheet.StoreSelectionGroup usa esa constante como MaxAreasPerRecord y escribe un grupo mayor como records Selection consecutivos del mismo panel, 1369 áreas cada 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 record que lo lleva. Una selección con un área más allá del límite lo deja claro: 1370 áreas con la última activa se convierten en dos records, el primero con cref 1369 y el segundo con cref 1, y ambos llevan irefAct 1369. Ese valor es mayor que el conteo de áreas del propio segundo record. Un lector que comprueba irefAct contra cref en cada record rechaza un archivo válido, y un lector que reemplaza su estado con cada record tira las primeras 1369 áreas. El lector de HotXLS anexa los records consecutivos del mismo panel en un solo grupo, exige que cada bloque coincida en celda activa e índice, y ejecuta la comprobación de rango solo en el record EOF de la hoja, cuando la secuencia completa ya se conoce. La sobrecarga de SelectAreas con el panel delante no tiene por tanto 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 anterior intacta si algo está mal formado

Por qué HotXLS escribe una selección de hoja grande como varios records Selection de BIFF8: el tope de cuerpo de 8.224 bytes admite 9 bytes fijos más 1369 áreas RefU de seis bytes, así que 3.000 áreas se convierten en tres records del mismo panel de 1369, 1369 y 262, y irefAct indexa la secuencia agregada, de modo que 1370 áreas con la última activa dejan ambos records con irefAct 1369
Cada bloque repite la misma celda activa e índice, el lector de HotXLS anexa los records consecutivos del mismo panel en un solo grupo, y la comprobación de rango se ejecuta solo en el record EOF, cuando la secuencia completa ya se conoce
var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Diffs: TXLSSelectedAreas;
  I: Integer;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add;
    Sheet.FreezePanes(1, 1);           // la fila de cabecera y la columna A se quedan quietas

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

    // Inmovilizar resetea la selección almacenada, así que selecciona después de inmovilizar.
    // 3000 áreas se guardan como tres records 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 record Selection?

Un record Selection identifica su panel por el código numérico que define el formato: 0 para bottom-right, 1 para top-right, 2 para bottom-left y 3 para top-left. La enumeración pública TXLSPanePosition está declarada en orden de lectura, xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight, así que Ord(xlspTopLeft) es 0, que en el archivo es el panel bottom-right. Fundir el enum directamente al byte de panel escribiría cada selección top-left sobre el panel bottom-right sin ningún error. Cada punto de entrada de HotXLS consciente de paneles convierte el enum mediante un case explícito, así que quien llama jamás trata con los códigos numéricos. También se comprueba la existencia del panel: el top-right solo existe con una división vertical, el bottom-left solo con una horizontal, y el bottom-right solo con ambas. Para un panel que la geometría actual de división o inmovilización no tiene, SelectAreas devuelve False, y GetSelectedAreas devuelve un array vacío con ActiveAreaIndex a -1, sin crear ni un panel, ni un objeto de selección, ni una celda en el workbook

Cómo mapea HotXLS TXLSPanePosition al byte de panel de Selection en BIFF8: el enum está declarado en orden de lectura, así que Ord(xlspTopLeft) es 0, mientras el archivo define 0 para bottom-right, 1 para top-right, 2 para bottom-left y 3 para top-left, de modo que cada punto de entrada consciente de paneles convierte mediante un case explícito
Fundir el enum directamente al byte de panel escribiría cada selección top-left sobre el panel bottom-right, así que HotXLS comprueba además la existencia del panel contra la geometría actual de división o inmovilización antes de escribir

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

La posición de scroll de cada panel se reparte entre dos records, porque cuatro paneles comparten solo dos posiciones de fila y dos de columna. En un workbook 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 por tanto Window2.rwTop y Pane.colLeft, y fijar la columna del panel top-right también mueve el bottom-right, igual que los dos comparten una sola barra de scroll horizontal en Excel. Los métodos públicos usan números de fila y columna 1-based. Un panel inexistente devuelve False y pone las dos salidas de la consulta a cero, y una coordenada fuera de rango se rechaza antes de que cambie cualquier eje. Nada de esto depende de cómo pinte la cuadrícula un visor. Un control de renderizado mantiene su propio TopRow y LeftCol, como describe el artículo sobre renderizar workbooks en una cuadrícula VCL personalizada, y eso es estado en tiempo de ejecución, 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 bottom-right siga
XLSX reparte estos 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 estos mismos datos entre dos elementos: sheetView/@topLeftCell (ECMA-376 Parte 1, §18.3.1.87) para la ventana como un todo, 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 exterior hacia los campos de nivel de ventana, deja que el hijo pane sobrescriba solo los campos de nivel de panel, y escribe ambos por separado de vuelta. 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 llevan ambas capas en los dos motores. Los puntos de entrada antiguos conservan su comportamiento original: las propiedades clásicas ScrollRow y ScrollColumn, y las SetPaneScroll y GetPaneScroll de XLSX 0-based. La geometría de inmovilización y división en sí se configura con los ajustes de nivel de hoja que cubre protección de hoja, configuración de página e impresión

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

  // Bottom-right: eje de fila inferior (Pane.rwTop) y eje de columna derecho (Pane.colLeft)
  Sheet.ScrollWindow(xlspBottomRight, 500, 3);

  // Top-right comparte el eje de columna derecho, así que esto también mueve bottom-right 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]));
    // Bottom-right empieza en la fila 500, columna 6
end;

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

Cuando un record 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 record se una al grupo de su panel, el lector lo comprueba en orden. El byte de panel debe ser 3 o menos. Los records 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 de un grupo debe coincidir en celda activa e irefAct, irefAct no debe traer el bit de signo activo, la columna activa debe estar en la cuadrícula, y ninguna área puede tener límites invertidos. Los problemas de un único record físico se reportan una vez por record. 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 en el EOF. Un grupo inválido queda invisible para la API tipada: GetSelectedAreas devuelve un array vacío con índice -1 para ese panel, mientras todos 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 borrar filas o columnas enteras remapea cada grupo de panel representado en ambos motores, el clásico y el XLSX, a través de un remapper compartido. Las áreas supervivientes conservan su orden y el área activa conserva su identidad. Si el área activa se borra, pasa a estar activa la primera sucesora superviviente, y si no hay nada detrás, la última antecesora superviviente. Si se borran todas las áreas, el grupo colapsa a una celda en el límite del borrado, 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 el remapper los salta en lugar de reescribirlos como una selección inventada, así que sus bytes originales siguen haciendo round-trip. Editar un panel reemplaza solo los records de ese panel y deja los demás byte a byte idénticos. ODS no recibe ningún estado de selección de paneles, porque ODF no tiene ninguna estructura equivalente de vista de hoja que lo lleve

Si tu aplicación escribe archivos .xls que los usuarios abren y necesitan recorrer, ya sea para revisar celdas marcadas, retomar donde lo dejaron o compartir un dashboard inmovilizado, 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 para XLSX