Artículo técnico

Registros Selection BIFF8 y scroll de paneles en HotXLS

Una herramienta de conciliación encuentra 3,000 celdas que no coinciden con el libro mayor y quiere que el .xls guardado abra con todas ellas seleccionadas debajo de una fila de encabezado congelada. Sobre el papel eso es un Selection record. En bytes son 9 + 3,000 × 6 = 18,009, y un body de record BIFF8 se detiene en 8,224. El mismo workbook plantea una segunda pregunta que suena trivial y no lo es: en una hoja congelada en B2, "la posición de scroll" son cuatro paneles compartiendo dos offsets de fila y dos offsets de columna, y esos cuatro números viven en dos records distintos

Los dos problemas son aritmética de formato: la selección se corta en records que un lector debe volver a coser, el panel se nombra con un código que no sigue el orden de lectura, y cada eje de scroll cae en el record desde el que Excel lo lee

Por qué 3,000 áreas seleccionadas necesitan tres Selection records

[MS-XLS] §2.4.248 define el Selection record (0x001D) como una parte fija de 9 bytes seguida por las áreas. La parte fija sostiene el byte de panel, rwAct y colAct para la celda activa, irefAct para el índice del área activa, y cref para el conteo de áreas. Cada área es una estructura RefU con dos filas de 16 bits y dos columnas de 8 bits. El conteo más grande que cabe es (8,224 − 9) / 6 redondeado hacia abajo, que da 1,369 áreas y un body de 8,223 bytes, un byte debajo del tope

HotXLS usa 1,369 como su techo por record. Cuando TXLSWorksheet guarda un grupo de selección con más áreas que eso, escribe Selection records consecutivos para el mismo panel, de a 1,369 áreas por vez, y cada record repite el mismo byte de panel, celda activa e índice activo. La selección de conciliación de 3,000 celdas sale como tres records que sostienen 1,369, 1,369 y 262 áreas. Los llamadores nunca ven esa partida; entregan un array de referencias A1

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 encabezado y la columna A se quedan quietas

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

    // FreezePanes limpia la selección guardada, así que seleccione después de congelar.
    // 3000 áreas se guardan como tres Selection records: 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;

El orden de las llamadas importa aquí. FreezePanes reconstruye el layout de paneles y limpia cada selección por panel que la hoja estaba cargando, así que una selección anterior ya se habría ido a la hora de guardar. En una hoja sin congelar el panel inferior derecho todavía no existe, y la misma llamada a SelectAreas simplemente devolvería False. Congele, luego seleccione

¿A qué apunta irefAct cuando una selección abarca varios records?

En una selección partida, irefAct indexa la secuencia agregada completa de áreas a través de todos los chunks, y cada chunk lleva el mismo valor. Una selección un área más allá del límite lo hace concreto: 1,370 áreas con la última activa se vuelven dos records, el primero con cref 1,369 y el segundo con cref 1, y ambos llevan irefAct 1,369. En el segundo record ese índice es mayor que el conteo de áreas del propio 'record'. Eso se ve roto si usted lee un record a la vez, y dos clases de lectores tropiezan con él

  • Un lector que chequea irefAct contra cref dentro de cada record rechaza un archivo perfectamente válido en el segundo chunk
  • Un lector que reemplaza su estado de selección en cada Selection record conserva solo el último chunk y bota las primeras 1,369 áreas

El lector de HotXLS agrega los records consecutivos del mismo panel en un solo grupo y exige que cada chunk concuerde en la fila activa, la columna activa y irefAct. El chequeo de rango sobre irefAct, y el chequeo de que la celda activa realmente se sienta dentro del área al que apunta, corren solo cuando llega el record de EOF del 'worksheet' y la secuencia completa es conocida. Chequear antes sería código más simple y la respuesta equivocada para cualquier selección de más de 1,369 áreas

Por qué HotXLS escribe una selección grande de worksheet como varios Selection records BIFF8: el tope de body de 8,224 bytes cabe 9 bytes fijos más 1369 áreas RefU de seis bytes, así que 3,000 áreas se vuelven tres records del mismo panel de 1369, 1369 y 262, e irefAct indexa la secuencia agregada, así que 1370 áreas con la última activa dan a ambos records irefAct 1369
Cada chunk repite la misma celda activa e índice, el lector de HotXLS agrega los records consecutivos del mismo panel en un solo grupo, y el chequeo de rango corre solo en el record de EOF una vez que la secuencia completa es conocida

Del lado de la escritura, la sobrecarga de SelectAreas con el panel primero no tiene techo propio de 1,369 áreas. Parsea cada referencia antes de tocar la hoja: los marcadores $ se aceptan y se quitan, un rango invertido como C9:A2 se normaliza a A2:C9, y una entrada vacía o no parseable falla la llamada completa. El índice activo tiene que caer dentro del array y quedarse en 32,767 o por debajo, porque irefAct es un campo con signo de 16 bits. Cualquier fallo devuelve False y deja la selección anterior exactamente como estaba

Los códigos de panel no siguen el orden de lectura

Un Selection record nombra su panel con el código numérico que el formato define: 0 para inferior derecho, 1 para superior derecho, 2 para inferior izquierdo y 3 para superior izquierdo. La enumeración pública TXLSPanePosition está declarada en orden de lectura, xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight. Así que Ord(xlspTopLeft) es 0, y 0 en el archivo significa inferior derecho. Un writer que castea la enum directo al byte de panel pone cada selección superior izquierda en el panel inferior derecho, y el archivo sigue siendo estructuralmente válido porque ambos son códigos de panel legales

Cada punto de entrada consciente de paneles en HotXLS convierte la enum a través de un case explícito, así que el código de aplicación nunca maneja los códigos numéricos. La existencia del panel también se chequea contra la geometría actual de congelado o split. El panel superior izquierdo siempre existe. El superior derecho necesita un split vertical, el inferior izquierdo uno horizontal, y el inferior derecho ambos. Para un panel que la hoja no tiene, SelectAreas devuelve False y GetSelectedAreas devuelve un array vacío con ActiveAreaIndex en -1, y ninguna de las dos llamadas crea un panel, una selección o una celda en el camino

Cómo HotXLS mapea TXLSPanePosition al byte de panel del Selection BIFF8: la enum está declarada en orden de lectura así que Ord(xlspTopLeft) es 0, mientras que el archivo define 0 para inferior derecho, 1 para superior derecho, 2 para inferior izquierdo y 3 para superior izquierdo, así que cada punto de entrada consciente de paneles convierte a través de un case explícito
Castear la enum directo al byte de panel escribiría cada selección superior izquierda en el panel inferior derecho, así que HotXLS también chequea la existencia del panel contra la geometría actual de split o congelado antes de escribir

El worksheet también conserva la SelectAreas(Areas, ActiveAreaIndex) de dos argumentos más vieja, que escribe en el panel que esté activo en ese momento y devuelve un estado Integer. En una hoja con paneles congelados eso suele ser una adivinanza sobre estado que usted no estableció. Use primero la sobrecarga que toma un TXLSPanePosition para cualquier cosa que corra sobre hojas congeladas o con split; el panel queda entonces parte de la llamada, y un resultado False le dice que la geometría y el pedido no concuerdan. Leer el resultado de vuelta sigue la misma forma:

var
  Areas: TXLSSelectedAreas;
  Active: Integer;
begin
  Areas := Sheet.GetSelectedAreas(xlspBottomRight, Active);
  if Active < 0 then
    Memo1.Lines.Add('No usable selection on the bottom-right pane')
  else
    Memo1.Lines.Add(Format('%d areas, active area %s',
      [Length(Areas), Areas[Active]]));
end;

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

La posición de scroll de cada panel está partida 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 de 'arriba' y la primera columna visible de los paneles de la 'izquierda' son Window2.rwTop y Window2.colLeft. La fila de los paneles de abajo y la columna de los paneles de la derecha son Pane.rwTop y Pane.colLeft. El superior derecho toma entonces su fila de Window2 y su columna de Pane, y el inferior izquierdo hace lo contrario

ScrollWindow y TryGetWindowScroll siguen ese layout exactamente y usan números de fila y columna base 1. ScrollWindow(xlspTopRight, R, C) escribe Window2.rwTop y Pane.colLeft, lo que significa que poner la columna del superior derecho también mueve el panel inferior derecho. Eso coincide con Excel, donde los dos paneles de la derecha comparten una barra de scroll horizontal. Un panel ausente hace que ScrollWindow devuelva False y que TryGetWindowScroll ponga ambas salidas en cero. Una fila o columna fuera de rango se rechaza antes de que cualquier eje cambie, así que una columna mala no puede dejar atrás una fila aplicada a medias

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

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

  // Superior derecho comparte el eje de columna derecho, así que esto también mueve inferior derecho 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 derecho arranca en la fila 500, columna 6
end;
Dónde vive cada eje de scroll de panel de 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 que 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 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 guarda las mismas dos capas en 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 un split. Ambos atributos pueden estar presentes a la vez. Un loader que conserva solo uno de ellos pierde las filas superiores o las columnas izquierdas de cada hoja congelada que toque. TXLSXWorksheet lee el atributo externo dentro de los campos a nivel de ventana primero, deja que el hijo pane sobrescriba solo los campos a nivel de panel, y escribe ambos de vuelta por separado. ScrollWindow y TryGetWindowScroll tienen las mismas firmas y significado en TXLSXWorksheet, y copiar un worksheet lleva ambas capas en cualquier formato

Las propiedades más angostas siguen ahí para código que solo necesita un par de ejes: ScrollRow y ScrollColumn en el worksheet clásico, y el SetPaneScroll y GetPaneScroll base 0 para el panel inferior derecho en XLSX. Un control de renderizado conserva su propio TopRow y LeftCol, como describe el artículo sobre renderizar workbooks en una grilla VCL personalizada, y ese estado de runtime es separado de lo que se guarda. La geometría de congelado y split en sí se establece con los ajustes a nivel de hoja cubiertos en protección de hoja, configuración de página e impresión

Los Selection records corruptos se conservan byte a byte

Cuando un Selection record está mal formado, HotXLS conserva su body como bytes opacos, reporta el código de diagnóstico 1304 (xlsDiagnosticSelectionRecordInvalid) como advertencia, y escribe el body original de vuelta sin cambios al guardar. Archivos de exporters desconocidos o writers BIFF hechos a mano pueden contener records así. Antes de que un record se una al grupo de su panel, el lector chequea un byte de panel de 3 o menos, records contiguos por panel, cref entre 1 y 1,369 con un body de exactamente 9 + cref × 6 bytes, chunks que concuerden en la celda activa y un irefAct sin su bit de signo, una columna activa en la grilla, y ninguna área invertida. Los problemas dentro de un record físico se reportan una vez por record; las contradicciones que solo aparecen después de 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 EOF

Un grupo inválido queda fuera de la API tipada: GetSelectedAreas devuelve un array vacío con índice -1 para ese panel, y los otros paneles siguen funcionando normal

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 es su propio TStrings; HotXLS solo llena Diagnostics
      Log.Add(Format('%s: record $%.4x kept opaque (%s)',
        [D.SheetName, D.RecordId, D.Message]));
  end;
end;

Reparar el record, clampeando irefAct o botando el área invertida, era la otra opción. Conservarlo es el default más seguro: una selección reparada es una que nadie eligió, y los bytes originales se habrían ido. El mensaje de diagnóstico nombra el chequeo que falló, como "el conteo de áreas no coincide con el largo del record" o "los records del mismo panel no son contiguos", y RecordId es siempre $001D para este código

Insertar filas y columnas remapea la selección de cada 'pane'

Insertar o borrar filas o columnas enteras remapea la selección de cada panel a través de una rutina compartida, en el motor clásico y en el XLSX. Las áreas sobrevivientes conservan su orden y se corren con las celdas que cubren, y el área activa conserva su identidad. Si el área activa se borra, la primera área sobreviviente después de ella se vuelve activa; si no sigue nada, lo hace la última área sobreviviente antes de ella. Si se borra cada área, el grupo colapsa a una sola celda en el borde del borrado. 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 jamás pueden contradecirse en el archivo guardado

Los grupos marcados como inválidos al cargar los deja tranquilos el remapeador, así que sus bytes originales siguen haciendo el viaje de ida y vuelta después de la edición. Cambiar la selección de un panel reescribe solo los records de ese panel y deja los records de los otros paneles idénticos byte a byte. Los archivos ODS no cargan ningún estado de selección de paneles, porque OpenDocument no tiene una estructura de vista de worksheet por panel equivalente; las selecciones y las posiciones de scroll de paneles aplican a XLS y XLSX

Si su aplicación guarda workbooks que la gente abre para revisar celdas marcadas, retomar donde lo dejaron, o leer un dashboard congelado, las llamadas de selección y scroll conscientes de paneles vienen con el componente de hojas de cálculo HotXLS para Delphi y se comportan igual para salida .xls y .xlsx