Una herramienta de conciliación encuentra 3,000 celdas que discrepan del mayor y quiere que el .xls guardado abra con todas ellas seleccionadas bajo una fila de cabecera congelada. Sobre el papel eso es un record Selection. En bytes son 9 + 3,000 × 6 = 18,009, y el cuerpo de un 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 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 tiene que volver a coser, el panel se nombra con un código que no sigue el orden de lectura, y cada eje de scroll aterriza en el record desde el que Excel lo lee
Por qué 3,000 áreas seleccionadas necesitan tres records Selection
[MS-XLS] §2.4.248 dispone el record Selection (0x001D) como una parte fija de 9 bytes seguida de las áreas. La parte fija lleva el byte de panel, rwAct y colAct de la celda activa, irefAct para el índice del área activa y cref para el recuento de áreas. Cada área es una estructura RefU con dos filas de 16 bits y dos columnas de 8 bits. El recuento más grande que cabe es (8,224 − 9) / 6 redondeado hacia abajo, lo que da 1,369 áreas y un cuerpo de 8,223 bytes, un byte por 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 ese número, escribe records Selection consecutivos para el mismo panel, 1,369 áreas cada 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 jamás ven ese corte; 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 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]);
// FreezePanes limpia la selección almacenada, así que seleccione tras congelar.
// 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;
El orden de las llamadas importa aquí. FreezePanes reconstruye la disposición de paneles y limpia cada selección por panel que la hoja llevara, así que una selección previa 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 SelectAreas devolvería False sin más. Primero 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 de 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 recuento de áreas del propio record. Parece roto si usted lee un record cada vez, y hay dos clases de lector que tropiezan con ello
- Un lector que comprueba
irefActcontracrefdentro de cada record rechaza un archivo perfectamente válido en el segundo chunk - Un lector que sustituye su estado de selección en cada record Selection conserva solo el último chunk y pierde las primeras 1,369 áreas
El lector de HotXLS añade los records consecutivos del mismo panel a un solo grupo y exige que cada chunk concuerde en la fila activa, la columna activa y irefAct. La comprobación de rango de irefAct, y la de que la celda activa se sienta de verdad dentro del área al que apunta, corren solo cuando llega el record EOF de la hoja y la secuencia completa es conocida. Comprobar antes sería código más sencillo y la respuesta equivocada para cualquier selección de más de 1,369 áreas
Por el lado de 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 imparsable falla la llamada entera. 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 previa exactamente como estaba
Los códigos de panel no siguen el orden de lectura
Un record Selection nombra su panel con el código numérico que define el formato: 0 para el inferior derecho, 1 para el superior derecho, 2 para el inferior izquierdo y 3 para el 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 escritor que haga un cast de la enumeración directo al byte de panel pone cada selección del panel superior izquierdo 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 enumeración 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 se comprueba además contra la geometría actual de congelación o división. El panel superior izquierdo existe siempre. El superior derecho necesita una división vertical, el inferior izquierdo una horizontal, y el inferior derecho ambas. Para un panel que la hoja no tiene, SelectAreas devuelve False y GetSelectedAreas devuelve un array vacío con ActiveAreaIndex puesto a -1, y ninguna de las dos llamadas crea un panel, una selección o una celda por el camino
La hoja también conserva el SelectAreas(Areas, ActiveAreaIndex) antiguo de dos argumentos, 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 apuesta sobre un estado que usted mismo no puso. Para cualquier cosa que corra sobre hojas congeladas o divididas, use primero la sobrecarga que recibe un TXLSPanePosition; el panel pasa a ser parte de la llamada, y un resultado False le dice que la geometría y la petición discrepan. La lectura 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 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 paneles izquierdos son Window2.rwTop y Window2.colLeft. La fila de los paneles inferiores y la columna de los paneles derechos son Pane.rwTop y Pane.colLeft. El superior derecho toma por tanto su fila de Window2 y su columna de Pane, y el inferior izquierdo al revés
ScrollWindow y TryGetWindowScroll siguen esa disposición exactamente y usan números de fila y columna 1-based. ScrollWindow(xlspTopRight, R, C) escribe Window2.rwTop y Pane.colLeft, lo que significa que fijar la columna del superior derecho también mueve el panel inferior derecho. Eso coincide con Excel, donde los dos paneles derechos comparten una única barra de scroll horizontal. Un panel ausente hace que ScrollWindow devuelva False y que TryGetWindowScroll ponga ambas salidas a cero. Una fila o columna fuera de rango se rechaza antes de que cambie cualquiera de los dos ejes, así que una columna mala no puede dejar una fila aplicada a medias
var
Row, Col: Integer;
begin
Sheet.FreezePanes(1, 1);
// Bottom-right: eje de fila inferior (Pane.rwTop) y eje de columna derecha (Pane.colLeft)
Sheet.ScrollWindow(xlspBottomRight, 500, 3);
// Top-right comparte el eje de columna derecha, 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;
XLSX guarda las mismas dos capas en 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. Un loader que se queda solo con uno de ellos pierde o las filas superiores o las columnas izquierdas de cada hoja congelada que toque. TXLSXWorksheet lee el atributo exterior en los campos de nivel de ventana primero, deja que el hijo pane sobrescriba solo los campos de nivel de panel, y escribe ambos de vuelta por separado. ScrollWindow y TryGetWindowScroll tienen las mismas firmas y el mismo significado en TXLSXWorksheet, y copiar una hoja lleva ambas capas en cualquiera de los dos formatos
Las propiedades más estrechas siguen ahí para el código que solo necesita un par de ejes: ScrollRow y ScrollColumn en la hoja clásica, y el SetPaneScroll y GetPaneScroll zero-based para el panel inferior derecho en XLSX. Un control de renderizado mantiene su propio TopRow y LeftCol, como describe el artículo sobre renderizar workbooks en una rejilla VCL personalizada, y ese estado de ejecución está separado de lo que se guarda. La geometría de congelación y división en sí se fija con los ajustes a nivel de hoja cubiertos en protección de hoja, configuración de página e impresión
Los records Selection corruptos se conservan byte a byte
Cuando un record Selection está malformado, HotXLS conserva su cuerpo como bytes opacos, reporta el código de diagnóstico 1304 (xlsDiagnosticSelectionRecordInvalid) como advertencia y vuelve a escribir el cuerpo original sin cambios al guardar. Archivos de exportadores desconocidos o de escritores BIFF hechos a mano pueden contener records así. Antes de que un record entre en el grupo de su panel, el lector comprueba un byte de panel de 3 o menos, records contiguos por panel, un cref entre 1 y 1,369 con un cuerpo de exactamente 9 + cref × 6 bytes, chunks que concuerdan en la celda activa y un irefAct sin su bit de signo, una columna activa sobre la rejilla, y ninguna área invertida. Los problemas dentro de un solo 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 recuento total de áreas o una celda activa fuera del área indexada, se reportan una vez por grupo en el EOF
Un grupo inválido se queda fuera de la API tipada: GetSelectedAreas devuelve un array vacío con índice -1 para ese panel, y los demás paneles siguen funcionando con normalidad
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, acotando irefAct o soltando 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 perdido. El mensaje de diagnóstico nombra la comprobación que falló, como «el recuento de áreas no coincide con la longitud del record» o «los records del mismo panel no son contiguos», y RecordId es siempre $001D para este código
Las inserciones de filas y columnas remapean la selección de cada panel
Insertar o borrar filas o columnas enteras remapea la selección de todos los paneles a través de una única rutina compartida, tanto en el motor clásico como en el XLSX. Las áreas supervivientes conservan su orden y se desplazan con las celdas que cubren, y el área activa conserva su identidad. Si el área activa se borra, la primera área superviviente detrás de ella pasa a activa; si no hay nada detrás, lo hace la última superviviente delante. Si se borran todas, el grupo colapsa a una sola celda en el límite 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 en la carga los deja en paz el remapeador, así que sus bytes originales siguen haciendo la 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 byte a byte idénticos. Los archivos ODS no llevan estado de selección por panel alguno, porque OpenDocument no tiene estructura equivalente de vista de hoja por panel; 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, continuar 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