Uno strumento di riconciliazione trova 3.000 celle in disaccordo con il ledger e vuole che il .xls salvato si apra con tutte quante selezionate sotto una riga di intestazione bloccata. Sulla carta è un record Selection. In byte sono 9 + 3.000 × 6 = 18.009, e un corpo di record BIFF8 si ferma a 8.224. Lo stesso workbook solleva una seconda domanda che sembra banale e non lo è: su un foglio bloccato a B2, "la posizione di scroll" sono quattro riquadri che condividono due offset di riga e due offset di colonna, e quei quattro numeri stanno in due record diversi
Entrambi i problemi sono aritmetica di formato: la selezione viene tagliata in record che un lettore deve ricucire insieme, il riquadro è nominato da un codice che non segue l'ordine di lettura, e ogni asse di scroll atterra nel record da cui Excel lo legge
Perché 3.000 aree selezionate richiedono tre record Selection
[MS-XLS] §2.4.248 compone il record Selection (0x001D) come una parte fissa di 9 byte seguita dalle aree. La parte fissa tiene il byte del riquadro, rwAct e colAct per la cella attiva, irefAct per l'indice dell'area attiva, e cref per il conteggio delle aree. Ogni area è una struttura RefU con due righe a 16 bit e due colonne a 8 bit. Il conteggio massimo che ci sta è (8.224 − 9) / 6 arrotondato per difetto, che dà 1.369 aree e un corpo di 8.223 byte, un byte sotto il tetto
HotXLS usa 1.369 come proprio tetto per record. Quando TXLSWorksheet salva un gruppo di selezione con più aree di così, scrive record Selection consecutivi per lo stesso riquadro, 1.369 aree alla volta, e ogni record ripete lo stesso byte di riquadro, cella attiva e indice attivo. La selezione di riconciliazione da 3.000 celle esce come tre record che tengono 1.369, 1.369 e 262 aree. I chiamanti non vedono mai quello split; consegnano un array di riferimenti A1
var
Book: TXLSWorkbook;
Sheet: TXLSWorksheet;
Diffs: TXLSSelectedAreas;
I: Integer;
begin
Book := TXLSWorkbook.Create;
try
Sheet := Book.Sheets.Add;
Sheet.FreezePanes(1, 1); // riga di intestazione e colonna A restano ferme
SetLength(Diffs, 3000);
for I := 0 to High(Diffs) do
Diffs[I] := Format('C%d', [I + 2]);
// FreezePanes pulisce la selezione conservata, quindi selezionate dopo il freeze.
// 3000 aree vengono salvate come tre record 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;
L'ordine delle chiamate conta qui. FreezePanes ricostruisce il layout dei riquadri e pulisce ogni selezione per riquadro che il foglio stava portando, quindi una selezione precedente se ne sarebbe andata al momento del salvataggio. Su un foglio non bloccato il riquadro in basso a destra non esiste ancora, e la stessa chiamata SelectAreas restituirebbe semplicemente False. Prima il freeze, poi la selezione
A cosa punta irefAct quando una selezione copre più record?
In una selezione divisa, irefAct indicizza l'intera sequenza aggregata di aree attraverso tutti i chunk, e ogni chunk porta lo stesso valore. Una selezione di un'area oltre il limite rende tutto concreto: 1.370 aree con l'ultima attiva diventano due record, il primo con cref 1.369 e il secondo con cref 1, e entrambi portano irefAct 1.369. Nel secondo record quell'indice è più grande del conteggio aree del record stesso. Sembra rotto se leggete un record alla volta, e due tipi di lettore ci inciampano
- Un lettore che controlla
irefActcontrocrefdentro ogni record respinge un file perfettamente valido al secondo chunk - Un lettore che sostituisce il proprio stato di selezione a ogni record Selection conserva solo l'ultimo chunk e butta via le prime 1.369 aree
Il lettore di HotXLS accoda i record consecutivi per lo stesso riquadro in un gruppo ed esige che ogni chunk concordi su riga attiva, colonna attiva e irefAct. Il controllo di intervallo su irefAct, e il controllo che la cella attiva stia davvero dentro l'area a cui punta, girano solo quando arriva il record EOF del worksheet e la sequenza completa è nota. Controllare prima sarebbe codice più semplice e la risposta sbagliata per qualsiasi selezione oltre 1.369 aree
Sul lato scrittura, l'overload di SelectAreas con il riquadro prima non ha un tetto proprio di 1.369 aree. Parsa ogni riferimento prima di toccare il foglio: i marcatori $ sono accettati e tolti, un intervallo invertito come C9:A2 viene normalizzato in A2:C9, e una voce vuota o non parsabile fa fallire l'intera chiamata. L'indice attivo deve cadere dentro l'array e restare a 32.767 o sotto, perché irefAct è un campo a 16 bit con segno. Qualsiasi fallimento restituisce False e lascia la selezione precedente esattamente com'era
I codici dei riquadri non seguono l'ordine di lettura
Un record Selection nomina il proprio riquadro con il codice numerico che il formato definisce: 0 per in basso a destra, 1 per in alto a destra, 2 per in basso a sinistra e 3 per in alto a sinistra. L'enumerazione pubblica TXLSPanePosition è dichiarata in ordine di lettura, xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight. Quindi Ord(xlspTopLeft) è 0, e 0 nel file significa in basso a destra. Uno scrittore che fa il cast dell'enumerazione dritto nel byte del riquadro mette ogni selezione in alto a sinistra sul riquadro in basso a destra, e il file resta strutturalmente valido perché entrambi sono codici di riquadro legali
Ogni punto di ingresso consapevole dei riquadri in HotXLS converte l'enumerazione attraverso uno statement case esplicito, così il codice applicativo non maneggia mai i codici numerici. L'esistenza del riquadro viene controllata anche contro la geometria corrente di freeze o split. Il riquadro in alto a sinistra esiste sempre. Quello in alto a destra richiede uno split verticale, quello in basso a sinistra uno orizzontale, e quello in basso a destra entrambi. Per un riquadro che il foglio non ha, SelectAreas restituisce False e GetSelectedAreas restituisce un array vuoto con ActiveAreaIndex a -1, e nessuna delle due chiamate crea nel frattempo un riquadro, una selezione o una cella
Il worksheet conserva anche la SelectAreas(Areas, ActiveAreaIndex) a due argomenti più vecchia, che scrive sul riquadro correntemente attivo e restituisce uno stato Integer. Su un foglio con riquadri bloccati è spesso un'ipotesi su stato che non avete impostato voi. Usate per primo l'overload che prende un TXLSPanePosition per qualsiasi cosa giri su fogli bloccati o divisi; il riquadro allora fa parte della chiamata, e un risultato False vi dice che geometria e richiesta sono in disaccordo. La rilettura del risultato segue la stessa 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;
Dove vive la posizione di scroll di ogni riquadro?
La posizione di scroll di ogni riquadro è divisa su due record, perché quattro riquadri condividono solo due posizioni di riga e due di colonna. In un workbook classico la prima riga visibile dei riquadri superiori e la prima colonna visibile dei riquadri a sinistra sono Window2.rwTop e Window2.colLeft. La riga dei riquadri inferiori e la colonna dei riquadri a destra sono Pane.rwTop e Pane.colLeft. Quello in alto a destra prende quindi la riga da Window2 e la colonna da Pane, e quello in basso a sinistra fa il contrario
ScrollWindow e TryGetWindowScroll seguono quel layout esattamente e usano numeri di riga e colonna a base 1. ScrollWindow(xlspTopRight, R, C) scrive Window2.rwTop e Pane.colLeft, il che significa che impostare la colonna del riquadro in alto a destra muove anche quello in basso a destra. Combacia con Excel, dove i due riquadri a destra condividono una sola barra di scorrimento orizzontale. Un riquadro mancante fa restituire a ScrollWindow False e fa impostare a TryGetWindowScroll entrambi gli output a zero. Una riga o colonna fuori intervallo viene respinta prima che uno dei due assi cambi, quindi una colonna sbagliata non può lasciare dietro una riga applicata a metà
var
Row, Col: Integer;
begin
Sheet.FreezePanes(1, 1);
// In basso a destra: asse riga inferiore (Pane.rwTop) e asse colonna destra (Pane.colLeft)
Sheet.ScrollWindow(xlspBottomRight, 500, 3);
// In alto a destra condivide l'asse colonna destra, quindi questo porta anche in basso a destra alla colonna 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]));
// In basso a destra parte dalla riga 500, colonna 6
end;
XLSX conserva gli stessi due layer in due elementi: sheetView/@topLeftCell (ECMA-376 Parte 1, §18.3.1.87) per la finestra nel suo complesso e il figlio pane/@topLeftCell (§18.3.1.66) per il lato in basso a destra di uno split. Entrambi gli attributi possono essere presenti insieme. Un loader che ne conserva solo uno perde o le righe superiori o le colonne sinistre di ogni foglio bloccato che tocca. TXLSXWorksheet legge l'attributo esterno nei campi a livello finestra per primo, lascia che il figlio pane faccia override dei soli campi a livello riquadro, e riscrive entrambi separatamente. ScrollWindow e TryGetWindowScroll hanno le stesse firme e lo stesso significato su TXLSXWorksheet, e copiare un worksheet porta entrambi i layer in entrambi i formati
Le proprietà più strette restano lì per il codice che richiede solo una coppia di assi: ScrollRow e ScrollColumn sul worksheet classico, e la SetPaneScroll e GetPaneScroll a base zero per il riquadro in basso a destra su XLSX. Un controllo di rendering tiene i propri TopRow e LeftCol, come descrive l'articolo sul rendering dei workbook in una griglia VCL personalizzata, e quello stato di runtime è separato da ciò che viene salvato. La geometria di freeze e split in sé si imposta con le impostazioni a livello foglio coperte in protezione del foglio, setup pagina e stampa
I record Selection corrotti vengono conservati byte per byte
Quando un record Selection è malformato, HotXLS conserva il suo corpo come byte opachi, riporta il codice diagnostico 1304 (xlsDiagnosticSelectionRecordInvalid) come avviso, e riscrive il corpo originale immutato al salvataggio. File da esportatori sconosciuti o scrittori BIFF artigianali possono contenere record simili. Prima che un record entri nel gruppo del proprio riquadro, il lettore controlla un byte di riquadro di 3 o meno, record contigui per riquadro, cref tra 1 e 1.369 con un corpo di esattamente 9 + cref × 6 byte, chunk concordi su cella attiva e un irefAct senza il bit di segno, una colonna attiva sulla griglia, e nessuna area invertita. I problemi dentro un singolo record fisico vengono riportati una volta per record; le contraddizioni che emergono solo dopo l'aggregazione, come un irefAct che punta oltre il conteggio totale di aree o una cella attiva fuori dall'area indicizzata, vengono riportate una volta per gruppo all'EOF
Un gruppo invalido resta fuori dall'API tipizzata: GetSelectedAreas restituisce un array vuoto con indice -1 per quel riquadro, e gli altri riquadri continuano a funzionare normalmente
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 è il vostro TStrings; HotXLS riempie solo Diagnostics
Log.Add(Format('%s: record $%.4x kept opaque (%s)',
[D.SheetName, D.RecordId, D.Message]));
end;
end;
Riparare il record, serrando irefAct o buttando l'area invertita, era l'altra opzione. Conservarlo è il default più sicuro: una selezione riparata è una che nessuno ha scelto, e i byte originali sarebbero andati persi. Il messaggio diagnostico nomina il controllo fallito, come "il conteggio aree non combacia con la lunghezza del record" o "i record per lo stesso riquadro non sono contigui", e RecordId è sempre $001D per questo codice
Gli inserimenti di righe e colonne rimappano la selezione di ogni riquadro
Inserire o eliminare righe o colonne intere rimappa la selezione di ogni riquadro attraverso una sola routine condivisa, sia nel motore classico sia in quello XLSX. Le aree sopravvissute conservano il proprio ordine e si spostano con le celle che coprono, e l'area attiva conserva la propria identità. Se l'area attiva è eliminata, la prima area sopravvissuta dopo di lei diventa attiva; se non c'è nulla dopo, lo fa l'ultima sopravvissuta prima. Se ogni area è eliminata, il gruppo collassa a una singola cella al confine dell'eliminazione. Una cella attiva che non cade più dentro l'area scelta si sposta all'angolo in alto a sinistra di quell'area, così indice e coordinata non possono mai contraddirsi nel file salvato
I gruppi marcati invalidi al caricamento vengono lasciati in pace dal remapper, così i loro byte originali fanno comunque il round trip dopo la modifica. Cambiare la selezione su un riquadro riscrive solo i record di quel riquadro e lascia i record degli altri riquadri identici byte per byte. I file ODS non portano alcuno stato di selezione per riquadro, perché OpenDocument non ha una struttura equivalente di vista del worksheet per riquadro; selezioni e posizioni di scroll dei riquadri valgono per XLS e XLSX
Se la vostra applicazione salva workbook che le persone aprono per rivedere celle segnalate, riprendere da dove avevano lasciato, o leggere una dashboard bloccata, le chiamate di selezione e scroll consapevoli dei riquadri viaggiano con l'HotXLS Delphi spreadsheet component e si comportano allo stesso modo per output .xls e .xlsx