En SpreadsheetML una columna que lleva un formato de moneda necesita exactamente un elemento: <col min="2" max="2" style="3"/>. ECMA-376 Part 1 adjunta un estilo de celda solo en tres lugares: una columna (§18.3.1.13), una fila (§18.3.1.73) y una celda (§18.3.1.4). Restrinja el formato a B2:D500000 y SpreadsheetML no tiene un solo elemento para esa forma: un estilo de columna correría más allá de la fila 500000 y un estilo de fila cubriría cada columna, así que un escritor acaba emitiéndolo como celdas de solo estilo, 1.499.997 de ellas
Un modelo de hoja de cálculo en memoria choca con la misma pared un paso antes. Si formatear significa poner una propiedad en cada objeto de celda, un clic en un encabezado de columna asigna un millón de objetos para cargar un pequeño entero, y un proceso Delphi que solo quería un formato de número se queda sin espacio de direcciones en un libro que en disco son unos pocos kilobytes. La pregunta real de diseño es dónde vive el formato mientras usted edita, y cuándo se convierte en datos por celda
Cómo HotXLS formatea un rango sin crear celdas
HotXLS conserva el formato de rangos como intervalos. Cada setter de estilo en TXLSXRange — SetNumberFormat, SetNumberFormatIndex, SetFontIndex, SetFillIndex, SetBorderIndex, SetAlignmentIndex — agrega un registro a la lista StyleOverlays de la hoja: cuatro coordenadas, las partes de estilo que la llamada posee y los índices de pool para esas partes. Las celdas que ya existen dentro del rectángulo levantan las partes nuevas de inmediato. Las coordenadas vacías se quedan vacías, y no se asigna nada para ellas
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Edge: TXLSXBorder;
BorderIdx, Row: Integer;
Stats: TXLSXCellStorageStatistics;
begin
Workbook := TXLSXWorkbook.Create;
try
Sheet := Workbook.Sheets.Add('Ledger');
Sheet.WriteStringRowValues(1, 2, ['Net', 'Tax', 'Gross']);
for Row := 2 to 1001 do
Sheet.WriteFloatRowValues(Row, 2, [Row * 1.5, Row * 0.3, Row * 1.8]);
// Columnas B..D sobre las 1.048.576 filas: un intervalo por llamada
Sheet.RCRange[1, 2, XlsxMaxRow, 4].SetNumberFormat('#,##0.00 "EUR"');
// Construya el borde una vez, y aplíquelo por índice
Edge := TXLSXBorder.Create;
try
Edge.SetAll(xlsxBorderThin);
BorderIdx := Workbook.Borders.LookupOrAdd(Edge); // 1-based
finally
Edge.Free;
end;
Sheet.RCRange[1, 2, XlsxMaxRow, 4].SetBorderIndex(BorderIdx);
Stats := Sheet.Cells.GetStorageStatistics;
// Espere 3.003: tres celdas de encabezado más 3.000 valores
WriteLn('Cells: ', Stats.CellCount);
if Workbook.SaveAs('ledger.xlsx') <> 1 then
raise Exception.Create('Save failed');
finally
Workbook.Free;
end;
end;
Las dos llamadas de formato cubrieron 3.145.728 coordenadas y no agregaron nada a ese conteo. Los intervalos empiezan en la fila 1 a propósito: un intervalo de columna completa se guarda como un elemento <col>, y los encabezados de texto en la fila 1 de todas formas no se ven afectados por un formato de número. Los rangos devueltos por RCRange, Range['A1:C5'], EntireColumn y EntireRow son propiedad de la hoja, así que el ejemplo jamás los libera
TXLSXFormatParts es lo que evita que varios intervalos se peleen. Sus miembros son xfpFont, xfpFill, xfpBorder, xfpNumberFormat, xfpAlignment, xfpProtection y xfpCellControl, y cada setter de TXLSXRange reclama exactamente uno de ellos. Cuando una celda se resuelve, los intervalos que la cubren se aplican en el orden en que se agregaron, cada uno escribiendo solo sus propias partes, y el formato explícito en la propia celda les gana a todos. Un intervalo de columna que fija el formato de número y un intervalo de fila que fija un relleno ambos llegan intactos a la celda B5
Cuando un registro debe cargar varias partes, Sheet.StyleOverlays.Add(Row1, Col1, Row2, Col2, Parts, State) toma un TXLSXCellStyleState con los índices de pool rellenados. Add acota las coordenadas a la cuadrícula de 1.048.576 × 16.384, intercambia esquinas invertidas, y devuelve nil sin registrar nada cuando Parts está vacío. Las ediciones estructurales mantienen los registros al día: InsertRows dentro de un bloque formateado ensancha el intervalo por el conteo insertado, y insertar encima desplaza el registro completo hacia abajo
¿Por qué SetBorders crea celdas cuando SetBorderIndex no?
TXLSXRange.SetBorders(Kind, Style, Color) tiene que mirar cada celda por su cuenta, porque la respuesta depende de la posición. Con xlsxEdgeOutline la fila superior recibe un borde superior, la columna izquierda un borde izquierdo, y el interior nada; con xlsxEdgeInside es más o menos al revés. La llamada también fusiona los bordes nuevos en cualquier borde que la celda ya tenga, así que Outline seguido de Inside construye una cuadrícula completa. Esa composición por celda pasa por Cells[R, C], que crea una celda cuando no existe. En un bloque de reporte de 10 × 40 esto es exactamente correcto. En EntireColumn(3) con xlsxEdgeAll son 1.048.576 objetos de celda nuevos
Así que la división es simple. Para cualquier cosa más alta que una pantalla de datos, construya el borde una vez y aplíquelo por índice, como hace el primer ejemplo: TXLSXBorder.SetAll, luego Workbook.Borders.LookupOrAdd para un índice 1-based, luego SetBorderIndex. Reserve SetBorders para bloques cuyo contorno y líneas internas difieren, como el bloque de reporte de 10 × 40 de arriba
Leer los formatos de vuelta sin deshacer el trabajo
La misma trampa atrapa al código que solo lee. Cells[R, C] es un accesor que crea al fallar: pregúntele el formato de número de B500000 y obtiene la respuesta correcta más una celda nueva, con estilo y vacía en B500000 que ahora se almacenará y guardará. Ponga ese bucle sobre una columna para auditar el formato y habrá reconstruido el millón de objetos que el intervalo quería evitar, una lectura de apariencia inocente a la vez
TXLSXWorksheet.GetCellVisibleFormat resuelve el estilo efectivo de una coordenada sin agregar una celda. Fusiona el predeterminado de la hoja, los intervalos de fila y columna, los intervalos de rectángulo, los estilos de tabla y cualquier formato explícito en la celda. El overload con un parámetro out ABuiltinNumberFormat reporta un formato integrado de Excel como 14 (fecha corta) por separado de NumberFormatIndex, que apunta al pool NumberFormats personalizado del libro. Cells.HasCell(R, C) responde si existe en absoluto una celda física
var
Sheet: TXLSXWorksheet;
Before, After: TXLSXCellStorageStatistics;
State: TXLSXCellStyleState;
Row, Builtin, Unformatted, Physical: Integer;
begin
Sheet := Workbook.Sheets[1]; // Sheets es 1-based
Before := Sheet.Cells.GetStorageStatistics;
Unformatted := 0;
Physical := 0;
for Row := 1 to 200000 do
begin
// Estilo efectivo, resuelto sin agregar una celda a la hoja
State := Sheet.GetCellVisibleFormat(Row, 2, Builtin);
if (State.NumberFormatIndex = 0) and (Builtin = 0) then
Inc(Unformatted);
if Sheet.Cells.HasCell(Row, 2) then
Inc(Physical);
// Sheet.Cells[Row, 2].NumberFormatIndex responde la misma pregunta,
// pero deja un objeto de celda nuevo detrás por cada fila que visita
end;
After := Sheet.Cells.GetStorageStatistics;
// Ambos conteos deberían ser idénticos antes y después del bucle
WriteLn(Format('cells %d -> %d, styled blanks %d -> %d',
[Before.CellCount, After.CellCount,
Before.StyledBlankCellCount, After.StyledBlankCellCount]));
end;
Corra una verificación así una vez contra su carga de trabajo real. StyledBlankCellCount cuenta celdas que tienen estilo y ningún valor, fórmula o texto enriquecido. Si sube durante una pasada que se suponía de solo lectura, algún helper está creando celdas puramente para cargar estilo, típicamente llamando a Cells[R, C] para decidir si una celda necesita formato
Qué significan los bloques de 256 filas para su código
Las celdas físicas viven en bloques de 256 filas. Dentro de un bloque, cada fila poblada guarda sus celdas en una lista ordenada por columna; una fila sin celdas es solo una ranura vacía, y un tramo de 256 filas sin celdas no tiene ningún bloque. Una hoja dispersa con datos en las filas 3, 7 y 512 por tanto sostiene dos bloques. El guardado recorre la misma estructura en orden fila-mayor, que es el orden que SpreadsheetML exige para los elementos <row> y sus hijos <c>
La referencia TXLSXCell que usted sostiene sigue siendo el mismo objeto mientras otras celdas se agregan o quitan a su alrededor; los bloques reorganizan su propia contabilidad, no sus handles. Los escritores en masa como WriteFloatRowValues y WriteStringRowValues toman una ruta más rápida en una hoja fresca y añaden valores fila-mayor a un almacén empaquetado. El primer acceso por celda vía Cells[R, C] convierte ese almacén en objetos de celda una sola vez. GetStorageStatistics muestra en qué estado está a través de PackedStorageActive y PackedCellCount, junto a RowBlockCount y PopulatedRowCount
Los pools de estilo se podan al momento de guardar. Antes de que styles.xml se escriba, HotXLS reconstruye los pools de fuente, relleno, borde, formato de número, alineación y protección para que contengan solo entradas aún en uso, y un libro que un usuario re-estilizó durante una hora no distribuye cada relleno abandonado. Las celdas ancladas a un FormatIndex absoluto conservan su ranura intacta
Guardados con streaming por filas y los números que lo muestran
TXLSXWorkbook.StreamingWrite vale True por defecto. Con él activo, SaveAs genera cada <row> de hoja y lo escribe directo al stream deflate de su entrada ZIP, así que el pedazo más grande de XML de hoja en memoria en cualquier momento es una sola fila. Con él apagado, el XML de cada hoja se construye primero como una cadena completa y se comprime después, y la memoria pico sigue a la hoja más grande. Las celdas de solo estilo en que se convierten los intervalos de rectángulo, el caso B2:D500000 de la apertura, se generan dentro de ese bucle por fila también, así que agregan al archivo pero jamás se sientan en memoria como un bloque de XML
Las shared strings y los registros de pivot cache siguen el mismo patrón. Cada entrada <si> o <r> se genera y se escribe por su cuenta, así que en un libro analítico ancho la tabla de shared strings, frecuentemente más grande que cualquier hoja individual, jamás tiene que existir como un bloque de XML
Nada de esto hay que tomarlo a la ciega. Después de cada guardado el libro expone cuatro propiedades de solo lectura: LastSaveUsedStreaming, LastSavePeakRowXmlChars (la fila única más larga, en caracteres), LastSaveBufferedWorksheetXmlChars (el XML total de hoja sostenido en memoria, cero cuando hay streaming) y LastSavePeakAuxiliaryXmlChars (la entrada de shared-string o pivot más larga). LastSaveUsedStreaming es True para salida .xlsx, .xltx y .xltm y para el escritor binario .xlsb; la salida OpenDocument va por su propio escritor y reporta False
procedure ReportSave(Workbook: TXLSXWorkbook; const FileName: string;
Streaming: Boolean);
begin
Workbook.StreamingWrite := Streaming;
if Workbook.SaveAs(FileName) <> 1 then
raise Exception.CreateFmt('Save failed: %s', [FileName]);
WriteLn(Format('streaming=%s peakRow=%d buffered=%d peakAux=%d',
[BoolToStr(Workbook.LastSaveUsedStreaming, True),
Workbook.LastSavePeakRowXmlChars,
Workbook.LastSaveBufferedWorksheetXmlChars,
Workbook.LastSavePeakAuxiliaryXmlChars]));
end;
// El mismo libro, ambos modos; observe la memoria del proceso en su profiler
ReportSave(Workbook, 'ledger-stream.xlsx', True);
ReportSave(Workbook, 'ledger-buffered.xlsx', False);
Sobre sus propios datos, la línea buffered muestra una cifra buffered del orden del XML de la hoja completa, mientras la línea streaming muestra cero y un peakRow no mayor que su fila más ancha. Deje StreamingWrite activo. La bandera decide solo si el XML de hoja se ensambla por completo antes de comprimir, y LastSaveBufferedWorksheetXmlChars reporta el tamaño de ese ensamblado en caracteres
A dónde va la memoria restante
Una vez que celdas y estilos están compactos, los siguientes consumidores más grandes en un libro grande son la tabla de shared strings y las partes satélite que el archivo cargue: pivot caches, dibujos y XML preservado de partes que el modelo de objetos no modela. Esas tienen sus propias palancas, y ninguna configuración única las cubre a todas
Si abrir es el cuello de botella en lugar de guardar, la carga selectiva es el lugar para empezar: el recorrido de carga de hojas solo con metadatos y selectiva cubre leer un libro sin pagar por hojas que usted no tocará. Para throughput de lectura en archivos muy grandes, vea las notas sobre parseo paralelo de XLSX y el allocador de memoria, y para trabajos de solo salida que nunca necesitan un modelo de objetos, escrituras con streaming para trabajos por lotes de servidor encaja mejor que cualquier cantidad de ajuste aquí
HotXLS lee y escribe XLS y XLSX desde código Delphi y C++Builder nativo sin Excel ni automatización OLE, así que cada número de arriba viene de su propio proceso y puede verificarse ahí. Los formatos soportados y las versiones de RAD Studio están listados en la página del componente de hojas de cálculo HotXLS para Delphi