HotXLS는 TXLSWorksheet와 TXLSXWorksheet 양쪽에서 pane을 인식하는 하나의 API로 워크시트 선택 영역과 스크롤 위치를 pane별로 저장합니다. SelectAreas, GetSelectedAreas, ScrollWindow, TryGetWindowScroll이 그것입니다. 클래식 .xls 파일의 경우 HotXLS는 각각 최대 1369개 영역을 담은 BIFF8 Selection 레코드(0x001D)를 쓰고, 논리적 pane 이름을 형식이 정의한 pane 바이트로 변환하며, 각 스크롤 축을 Excel이 기대하는 Window2 또는 Pane 레코드에 둡니다
문제는 보통 장부 대사 도구나 감사 도구에서 드러납니다. 도구는 장부 내보내기를 열어 원본 시스템과 어긋나는 모든 셀을 찾고, 헤더 행을 고정한 상태로 그 셀들이 선택된 채 통합 문서를 저장해서 검토자가 스크롤로 차이점을 찾아 헤매는 대신 그 자리로 바로 떨어지게 합니다. 차이가 40개면 아무 문제 없습니다. 월말 파일에는 3,000개가 있는데, 3,000개 영역을 담은 Selection 레코드 하나는 존재할 수 없습니다. 본문에 18,009바이트가 필요한데, 이는 BIFF8 레코드 하나가 실을 수 있는 양의 두 배가 넘습니다. 스크롤 위치에도 비슷한 함정이 있습니다. 틀을 고정한 시트에서 "사용자가 보고 있던 곳"은 좌표 하나가 아니라 두 개의 행 위치와 두 개의 열 위치를 공유하는 네 개의 pane입니다
큰 선택 영역은 왜 Selection 레코드 하나로는 부족할까요?
큰 선택 영역에 여러 레코드가 필요한 이유는 BIFF8 레코드 본문이 8224바이트로 제한되고 선택 영역 하나가 고정 6바이트를 먹기 때문입니다. [MS-XLS] §2.4.248은 Selection 레코드를 9바이트 고정부(pane 바이트, 활성 셀의 rwAct와 colAct, 활성 영역의 irefAct, 영역 개수의 cref)와 그 뒤를 따르는 cref개의 RefU 구조로 배치합니다. 각 RefU는 16비트 행 두 개와 8비트 열 두 개를 담습니다. 들어가는 최대 개수는 (8224 − 9) / 6을 내림한 1369이고, 이때 본문은 8223바이트로 한도 바로 아래입니다. TXLSWorksheet.StoreSelectionGroup은 이 상수를 MaxAreasPerRecord로 쓰고, 그보다 큰 그룹은 같은 pane의 Selection 레코드를 이어 쓰는 방식으로 한 번에 1369개씩 기록합니다
살을 파고드는 디테일이 irefAct입니다. 각 청크는 같은 활성 행, 활성 열, 활성 영역 인덱스를 반복하는데, irefAct는 그 레코드 안의 영역이 아니라 모든 청크를 합친 시퀀스의 인덱스입니다. 한도를 딱 한 영역 넘는 선택이 이를 잘 보여 줍니다. 마지막 영역이 활성인 1370개 영역은 레코드 두 개가 되는데, 첫 번째는 cref 1369, 두 번째는 cref 1이면서 둘 다 irefAct 1369를 가집니다. 이 값은 두 번째 레코드 자체의 영역 개수보다 큽니다. 레코드마다 irefAct를 cref와 비교하는 리더는 정상 파일을 거부하고, 레코드마다 상태를 갈아끼우는 리더는 첫 1369개 영역을 잃습니다. HotXLS 리더는 이어지는 같은 pane 레코드들을 한 그룹으로 붙이고, 모든 청크가 활성 셀과 인덱스에 대해 일치할 것을 요구하며, 범위 검사는 전체 시퀀스를 알게 되는 워크시트 EOF 레코드에서만 돌립니다. 그래서 pane을 먼저 받는 SelectAreas 오버로드에는 1369개 천장이 없습니다. 워크시트 쓰기 잠금을 잡기 전에 모든 A1 참조와 활성 인덱스를 검증하고, 뭔가 잘못되면 이전 선택을 그대로 둔 채 False를 반환합니다
var
Book: TXLSWorkbook;
Sheet: TXLSWorksheet;
Diffs: TXLSSelectedAreas;
I: Integer;
begin
Book := TXLSWorkbook.Create;
try
Sheet := Book.Sheets.Add;
Sheet.FreezePanes(1, 1); // 헤더 행과 A열은 제자리
SetLength(Diffs, 3000);
for I := 0 to High(Diffs) do
Diffs[I] := Format('C%d', [I + 2]);
// 틀 고정은 저장된 선택을 리셋하므로 고정한 다음에 선택합니다.
// 3000개 영역은 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;
Selection 레코드는 어떤 pane 바이트를 쓸까요?
Selection 레코드는 형식이 정의한 숫자 코드로 자기 pane을 식별합니다. 오른쪽 아래가 0, 오른쪽 위가 1, 왼쪽 아래가 2, 왼쪽 위가 3입니다. 공개 열거형 TXLSPanePosition은 읽는 순서대로 xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight로 선언되어 Ord(xlspTopLeft)가 0인데, 파일에서 그건 오른쪽 아래 pane입니다. 열거형을 pane 바이트에 그대로 캐스팅하면 왼쪽 위 선택이 전부 오류 없이 오른쪽 아래 pane에 써집니다. 그래서 pane을 인식하는 모든 HotXLS 진입점은 열거형을 명시적 case 문으로 변환하며, 호출자는 숫자 코드를 전혀 다루지 않습니다. pane 존재 여부도 검사합니다. 오른쪽 위 pane은 세로 분할일 때만, 왼쪽 아래는 가로 분할일 때만, 오른쪽 아래는 둘 다일 때만 존재합니다. 현재 분할이나 고정 기하가 갖고 있지 않은 pane에 대해서는 SelectAreas가 False를 반환하고 GetSelectedAreas는 ActiveAreaIndex를 -1로 한 빈 배열을 반환하며, pane이나 선택 객체, 통합 문서의 셀을 새로 만들지 않습니다
각 pane의 스크롤 위치는 어디에 살까요?
각 pane의 스크롤 위치는 두 레코드에 나뉘어 있습니다. 네 개의 pane이 두 개의 행 위치와 두 개의 열 위치만 공유하기 때문입니다. 클래식 통합 문서에서 위쪽 pane들의 첫 보이는 행과 왼쪽 pane들의 첫 보이는 열은 Window2.rwTop과 Window2.colLeft이고, 아래쪽 pane들의 행과 오른쪽 pane들의 열은 Pane.rwTop과 Pane.colLeft입니다. 따라서 ScrollWindow(xlspTopRight, R, C)는 Window2.rwTop과 Pane.colLeft를 쓰고, 오른쪽 위 pane의 열을 설정하면 오른쪽 아래 pane도 움직입니다. Excel에서 둘이 가로 스크롤바 하나를 공유하는 것과 똑같습니다. 공개 메서드는 1 기반 행·열 번호를 씁니다. 없는 pane은 False를 반환하며 질의 출력 둘 다 0으로 설정하고, 범위를 벗어난 좌표는 어느 축도 바뀌기 전에 거부됩니다. 여기에는 뷰어가 그리드를 그리는 방식이 전혀 개입하지 않습니다. 렌더링 컨트롤은 자기만의 TopRow와 LeftCol을 유지하는데, 커스텀 VCL 그리드로 통합 문서 렌더링하기 글이 설명하듯 그것들은 런타임 상태이지 저장되는 것이 아닙니다
XLSX는 같은 데이터를 두 요소에 나눕니다. 창 전체에는 sheetView/@topLeftCell(ECMA-376 Part 1, §18.3.1.87), 분할의 오른쪽 아래 쪽에는 자식 pane/@topLeftCell(§18.3.1.66)입니다. 두 속성은 동시에 존재할 수 있습니다. HotXLS는 바깥 속성을 먼저 창 수준 필드로 읽고, pane 자식이 pane 수준 필드만 덮어쓰게 한 뒤, 둘을 따로 되씁니다. 두 층을 하나로 합쳐 버리는 것이 로드 시 위쪽이나 왼쪽 스크롤 위치가 조용히 사라지는 바로 그 이유입니다. 워크시트 복사는 두 엔진 모두에서 두 층을 함께 옮깁니다. 이전 진입점들은 원래 동작을 유지합니다. 클래식 ScrollRow와 ScrollColumn 속성, 그리고 0 기반 XLSX SetPaneScroll과 GetPaneScroll입니다. 틀 고정과 분할 기하 자체는 시트 보호, 페이지 설정, 인쇄에서 다루는 시트 수준 설정으로 구성합니다
var
Row, Col: Integer;
begin
Sheet.FreezePanes(1, 1);
// 오른쪽 아래: 아래쪽 행 축(Pane.rwTop)과 오른쪽 열 축(Pane.colLeft)
Sheet.ScrollWindow(xlspBottomRight, 500, 3);
// 오른쪽 위는 오른쪽 열 축을 공유하므로 이 호출로 오른쪽 아래도 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]));
// 오른쪽 아래는 500행 6열에서 시작
end;
Selection 레코드가 손상되면 어떻게 될까요?
Selection 레코드가 손상되면 HotXLS는 그것을 불투명 바이트로 보관하고, 진단 코드 1304(xlsDiagnosticSelectionRecordInvalid)를 보고하며, 저장 시 원본 본문을 바이트 단위로 그대로 되씁니다. 레코드가 자기 pane의 그룹에 합류하기 전에 리더는 순서대로 검사합니다. pane 바이트는 3 이하여야 합니다. 같은 pane의 레코드들은 스트림에서 연속적이어야 합니다. 9바이트 고정부가 존재해야 합니다. cref는 1 이상 1369 이하여야 하고, 본문 길이는 정확히 9 + cref × 6바이트여야 합니다. 그룹의 모든 청크는 활성 셀과 irefAct에 대해 일치해야 하고, irefAct는 부호 비트가 켜져 있으면 안 되며, 활성 열은 그리드 위에 있어야 하고, 어떤 영역도 뒤집힌 경계를 가져서는 안 됩니다. 단일 물리 레코드 안의 문제는 레코드당 한 번 보고됩니다. irefAct가 전체 영역 개수를 넘거나 활성 셀이 인덱싱된 영역 밖에 있는 것처럼 합산 후에야 드러나는 모순은 EOF에서 그룹당 한 번 보고됩니다. 무효한 그룹은 타입 API에는 보이지 않습니다. 해당 pane에 대해 GetSelectedAreas는 인덱스 -1의 빈 배열을 반환하고, 다른 pane들은 계속 동작합니다
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.Add(Format('%s: record $%.4x kept opaque (%s)',
[D.SheetName, D.RecordId, D.Message]));
end;
end;
선택 영역은 행·열 삽입을 어떻게 견뎌낼까요?
선택 영역은 구조 편집을 견뎌냅니다. 행이나 열 전체를 삽입 또는 삭제하면 공유 리맵퍼 하나가 클래식과 XLSX 엔진 양쪽에서 표현된 모든 pane 그룹을 리맵하기 때문입니다. 살아남은 영역은 순서를 유지하고, 활성 영역은 정체를 유지합니다. 활성 영역이 삭제되면 첫 번째 살아남은 후계 영역이 활성이 되고, 뒤가 없으면 마지막 살아남은 선행 영역이 됩니다. 모든 영역이 삭제되면 그룹은 삭제 경계의 셀 하나로 무너지고, 더 이상 선택된 영역 안에 있지 않은 활성 셀은 그 영역의 왼쪽 위 모서리로 옮겨져 인덱스와 좌표가 서로 모순되는 일이 없습니다. 한계는 의도적입니다. 무효한 클래식 그룹은 리맵퍼가 건너뛰지 만들어낸 선택으로 다시 쓰지 않으므로 원본 바이트가 그대로 왕복합니다. 한 pane을 편집하면 그 pane의 레코드만 갈아끼우고 나머지는 바이트 단위로 동일하게 유지됩니다. ODS는 pane 선택 상태를 전혀 갖지 않습니다. ODF에는 그것을 실을 만한 대응되는 워크시트 뷰 구조가 없기 때문입니다
애플리케이션이 사용자가 열어서 둘러봐야 하는 .xls 파일 — 표시된 셀 검토든 멈춘 지점에서 이어서 작업이든 고정된 대시보드 공유든 — 을 쓴다면, pane을 인식하는 선택·스크롤 API가 HotXLS Delphi 스프레드시트 컴포넌트의 일부이며 XLS와 XLSX에서 똑같이 동작합니다