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
irefActcontracrefdentro 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
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
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;
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