기술 문서

Delphi에서 Excel 테이블 Slicer 만들기: HotXLS 왕복 처리

HotXLS는 Delphi와 C++Builder에서 한 번의 호출 — Worksheet.Slicers.Add(Table, ColumnName) — 로 Excel 테이블 slicer를 만들고, slicer가 필요로 하는 패키지 위치 여덟 곳을 모두 기록합니다. slicer와 slicerCache 파트, 두 콘텐츠 타입 오버라이드, 매칭되는 extLst 블록이 있는 워크시트 및 통합 문서 릴레이션십, 그리고 드로잉 앵커입니다

Slicer는 시트에 떨어뜨려 놓는 컨트롤처럼 보입니다. 파일 안에서는 전혀 그렇지 않습니다. 두 개의 새 파트, 두 개의 릴레이션십 그래프, 세 개의 확장 블록, 하나의 드로잉 앵커에 걸쳐 흩어진 분산 레코드이며, Excel이 패키지를 열 때 전체가 하나의 단위로 검증됩니다. 한 조각이라도 빠지면 기능이 줄어든 slicer가 아니라 복구 대화상자가 나오고, 이어서 slicer가 조용히 제거된 통합 문서를 보게 됩니다. 그 실패 모드가 slicer를 손으로 작성하기 귀찮게 만드는 이유이고, 여기서 흥미로운 엔지니어링이 렌더링이 아니라 부기(bookkeeping)인 이유입니다

하나의 slicer가 패키지의 여덟 곳에 닿는 이유

Slicer는 OPC가 세 개의 별도 집으로 강제하는 세 가지 개념이기 때문입니다. 필터 상태(통합 문서 범위), 뷰(워크시트 범위), 셰이프(드로잉 범위)입니다. HotXLS는 테이블 slicer당 정확히 여덟 개의 착륙 지점을 기록하고, 그 각각이 하중을 견딥니다

  • xl/slicers/slicerN.xml — 워크시트당 하나의 파트로, 그 시트의 모든 <slicer> 뷰를 http://schemas.microsoft.com/office/spreadsheetml/2009/9/main 네임스페이스에 담습니다
  • xl/slicerCaches/slicerCacheN.xml — 통합 문서 레벨로, 슬라이스된 테이블 열당 하나씩이며 실제 바인딩을 담습니다
  • 그 두 파트 각각에 대해 [Content_Types].xml에 <Override> 하나씩, application/vnd.ms-excel.slicer+xml과 application/vnd.ms-excel.slicerCache+xml 사용 (ECMA-376 Part 2는 모든 파트에 콘텐츠 타입을 요구하며, 없으면 Excel이 패키지를 거부합니다)
  • 타입이 .../office/2007/relationships/slicer인 워크시트 릴레이션십 하나, 그리고 그 rid를 가리키는 워크시트 extLst 안의 <x14:slicerList> 확장
  • 타입이 .../office/2007/relationships/slicerCache인 통합 문서 릴레이션십 하나, 그리고 같은 rid들을 나열하는 통합 문서 extLst 안의 <x15:slicerCaches> 확장
  • 시트 드로잉의 mc:AlternateContent 앵커로 slicer의 위치와 크기를 부여합니다
  • 캐시 파트 안의 <x15:tableSlicerCache> 블록으로 테이블 id와 테이블 열 id에 바인딩합니다

확장 블록은 표준 ECMA-376 Part 1 §18.2.10 extLst 메커니즘을 사용합니다. 각 <ext>는 기능을 식별하는 uri 속성을 지니고, GUID를 인식하지 못하는 리더는 블록을 건너뜁니다. HotXLS는 그 GUID만으로 테이블 slicer와 피벗 slicer를 구분합니다. 테이블 slicerList는 {3A4CF648-6AED-40f4-86FF-DC5316D8AED3}, 피벗은 {A8765BA9-456A-4dab-B4F3-ACF838C121DE}, 그리고 두 통합 문서 slicerCaches 확장은 각각 {46BE6895-7355-4a93-B00E-2C351335B9C9}와 {BBE1A952-AA13-448e-AADC-164F8A28A991}입니다. 테이블 바인딩 자체는 {2F2917AC-EB37-4324-AD4E-5DD8C200BD13} 아래에 있습니다. 그 상수들은 유닛에서 XlsxExtUriSlicerListX15, XlsxExtUriWbSlicerCachesX15, XlsxExtUriTableSlicerCache로 노출되므로, 쓰기 경로 어디에도 매직 문자열이 없습니다

Worksheet.Slicers.Add로 만든 하나의 테이블 slicer에 대해 HotXLS가 기록하는 여덟 패키지 위치: slicer와 slicerCache 파트, 두 콘텐츠 타입 오버라이드, 워크시트 및 통합 문서 릴레이션십, x14:slicerList와 x15:slicerCaches extLst 블록, 드로잉 앵커로, 각각 Excel이 패키지를 검증할 때 하중을 견딥니다
Worksheet.Slicers.Add 호출 하나가 패키지 전역의 여덟 착륙 지점으로 퍼집니다. 두 파트, 두 콘텐츠 타입 오버라이드, 두 릴레이션십, 두 extLst 블록, 하나의 드로잉 앵커로, Excel이 파일을 열 때 각각 하나의 단위로 검증됩니다

어떤 파트는 그대로 재생할 수 있고 어떤 파트는 다시 빌드해야 하는가

HotXLS는 단 하나의 규칙으로 답합니다. 참조하는 것 중 저장 중에 번호가 다시 매겨질 수 있는 것이 하나라도 있으면 파트는 원본 바이트를 재생할 수 없습니다. 테이블 slicer 캐시는 그 검사를 통과하지 못하므로, HotXLS는 사용자가 slicer를 건드리지 않았어도 매 저장마다 모델에서 무조건 다시 빌드합니다. 이유는 tableId입니다. <x15:tableSlicerCache> 요소는 <table>의 id 속성으로 자기 테이블을 식별하는데, HotXLS는 쓸 때 테이블 id를 통합 문서 전역에서 다시 매깁니다. 초기 구현은 열 때 캐시 파트를 원시 바이트로 저장해 두고 저장 시 그대로 밀어 넣었는데, 이것은 대부분의 불투명 파트에 맞는 전략입니다. 그렇게 만든 파일은 일반적인 경우에는 깨끗하게 열렸지만, 흥미로운 경우에는 복구를 유발했습니다. 앞선 시트에 테이블을 하나 추가하고 저장하면, 캐시가 이제 다른 무언가에 속한 테이블 id를 가리키거나 아무것도 가리키지 않게 됩니다. 그 버그는 평범한 열고-저장 회귀 테스트에서는 보이지 않는데, id가 우연히 움직이지 않기 때문입니다

피벗 형태의 캐시는 반대 길을 택합니다. 그 페이로드는 <pivotTables>와, 캐시된 항목, 정렬 상태, 항목 인덱스를 기술하는 <x14:data> 블록입니다. HotXLS 쪽에서 번호가 다시 매겨질 것이 없는 구조이고, 충실히 모델링하기엔 비용이 큰 구조입니다. 그것들은 원시 바이트로 유지되어 정확히 재생되며, TXLSXSlicerCache.IsPivot와 IsTableCache가 둘을 구분합니다. slicer 뷰는 세 번째 정책, 더티 트래킹을 따릅니다. 건드리지 않은 <slicer> 요소와 그 드로잉 앵커는 그대로 재생되고, 시트의 어떤 뷰라도 수정되는 순간 전체 시트 파트가 모델에서 다시 생성됩니다. 이것이 판단의 일반적인 형태입니다. 참조가 안정적이면 그대로 재생, 작성자가 정체성을 다시 부여하면 모델 재빌드, 둘 다 허용 가능하면 더티 트래킹입니다. 그리고 이는 slicer를 훨씬 넘어 적용됩니다. 테마, extLst 블록, calcChain에 관한 무손실 왕복 노트가 더 깊이 다룹니다

HotXLS의 slicer 파트별 재생 정책: 테이블 slicer 캐시는 저장 시 tableId 값이 통합 문서 전역에서 다시 매겨지므로 항상 모델에서 다시 빌드되고, 피벗 캐시는 pivotTables와 x14:data가 다시 매겨지는 법이 없어 원시 바이트를 재생하며, slicer 뷰는 더티 트래킹되어 시트의 어떤 뷰가 바뀌기 전까지 그대로 재생됩니다
파트별 재생 정책입니다. 테이블 slicer 캐시는 테이블 id가 통합 문서 전역에서 다시 매겨지므로 항상 재빌드되고, 피벗 캐시는 안에 다시 매겨지는 것이 없어 원시 바이트를 재생하며, 뷰는 더티 트래킹되어 건드리지 않은 저장이 바이트 단위로 깨끗하게 유지됩니다

Delphi에서 테이블 slicer 만들기

TXLSXSlicers.Add가 생성 API의 전부입니다. TXLSXTable과 열 이름이 주어지면 테이블 열 id를 구체화하고, 뒷받침하는 TXLSXSlicerCache를 찾거나 만들며, 매칭되는 정의된 이름을 예약하고, 테이블 오른쪽에 뷰를 앵커링합니다. 두 인자 오버로드는 배치를 정하고, 여섯 인자 오버로드는 명시적인 1-based 포함 박스를 받습니다

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Slicer: TXLSXSlicer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('sales.xlsx');
    Sheet := Book.Sheets[0];

    // 기본 배치: 테이블에서 두 열 떨어진 위치,
    // 19pt 기본 행 높이로 13행 높이
    Slicer := Sheet.Slicers.Add(Sheet.Tables[0], 'Region');
    Slicer.Caption   := 'Sales region';
    Slicer.StyleName := 'SlicerStyleLight1';
    Slicer.ColumnCount := 2;

    Book.SaveAs('sales-sliced.xlsx');
  finally
    Book.Free;
  end;
end;

알아 둘 가치가 있는 두 가지 명명 규칙이 있습니다. HotXLS의 발명이 아니라 Excel 관례입니다. 캐시는 Slicer_<column>으로 이름 지어지고 식별자로 쓸 수 없는 문자는 밑줄로 접히며, 충돌하면 숫자 접미사가 붙습니다. Slicer_Region, 이어서 Slicer_Region1 같은 식입니다. 그리고 모든 캐시 이름은 수식이 #N/A인 통합 문서 정의 이름으로도 예약되는데, TXLSXSlicers.ReserveCacheDefinedName이 하는 일이 바로 그것입니다. Excel이 그 이름을 스스로 만들며, 이는 중요합니다. 캐시 이름과 정의 이름은 네임스페이스를 공유하므로, 이미 Slicer_Region이라는 이름이 있는 통합 문서는 다음 저장에서 충돌하게 됩니다. 테이블과 데이터 검증을 함께 구축한다면, 데이터 검증, AutoFilter, 테이블에 관한 문서가 인접한 정의 이름 상호작용을 다룹니다. 공유도 Excel이 쓰는 것과 같은 규칙을 따릅니다. 새 캐시를 만들기 전에 TXLSXSlicerCaches.FindForTableColumn(ATableId, ATableColumnId)를 먼저 조회하므로, 같은 테이블 열 위의 두 slicer 뷰 — 대시보드 시트 하나, 상세 시트 하나 — 는 단일 캐시 파트를 공유하고 필터 동기화가 유지됩니다. 뷰 삭제는 통합 문서의 모든 시트를 상대로 역방향 검사를 실행하고, 그 뷰를 참조하는 마지막 뷰가 사라졌을 때만 캐시와 예약된 #N/A 이름을 버립니다

양쪽 끝에서 릴레이션십 id가 안정적으로 유지되는 방식

HotXLS가 다른 모든 릴레이션십을 방출하는 정확한 순서를 재생해 slicer rid를 계산하고, 새 릴레이션십을 꼬리에 덧붙이기 때문에 안정적입니다. 파일에 이미 있던 것은 아무것도 움직이지 않습니다. 대안을 디버깅해 보기 전까지는 과잉 엔지니어링처럼 들립니다. 워크시트 .rels의 릴레이션십 id는 워크시트 파트 내부에서 참조됩니다. 하이퍼링크, 드로잉, 주석, 테이블 파트로 말이죠. ECMA-376 Part 2는 그것들을 안정적인 정체성이 아니라 릴레이션십 그래프 안에서 위치적으로 해석합니다. 작성자가 slicer 릴레이션십을 중간에 끼워 넣으면 그 뒤의 모든 참조가 하나씩 밀리고, 시트가 조용히 잘못된 파트를 가리키기 시작합니다. 그래서 XlsxWorksheetSlicerRelNumber는 결정론적 체인 전체를 훑습니다. 외부 하이퍼링크, 주석 VML, 주석, 머리글/바닥글 이미지, 스레드 주석, 드로잉, 시트 배경, 프린터 설정, 테이블, 피벗 테이블, 쿼리 테이블, 테이블 단일 셀 순이며, 그제서야 slicer용 다음 번호를 가져갑니다. XlsxWorkbookSlicerCacheFirstRid는 통합 문서 쪽에서 똑같이 합니다. 시트, sharedStrings, 스타일, VBA, 외부 링크, 테마, 연결, 피벗 캐시, person, calcChain, 시트 메타데이터, XML 맵, 커스텀 XML, 그다음 slicerCache 묶음입니다. 일반적인 문제는 OPC 릴레이션십 해석에 관한 글에서 다룹니다. 통합 문서 쪽 확장은 그 번호들로 XlsxBuildWorkbookSlicerCachesExtXml이 만들고, 소스 파일에 이미 slicerCaches 확장 블록이 있었다면 먼저 원시 extLst에서 제거됩니다. 그래서 모델이 오래된 복사본을 남기는 대신 현재 rid에 맞춰 다시 방출합니다

HotXLS가 slicer 릴레이션십 id를 안정적으로 유지하는 방식: XlsxWorksheetSlicerRelNumber가 외부 하이퍼링크부터 드로잉을 거쳐 테이블 단일 셀까지 결정론적 워크시트 체인을 훑고 slicer 릴레이션십을 꼬리에 덧붙이는 동안, XlsxWorkbookSlicerCacheFirstRid가 통합 문서 쪽에서 같은 일을 해서 삽입이 기존 rid 참조를 절대 밀지 않습니다
릴레이션십 id는 위치적으로 해석되므로, slicer 릴레이션십은 양쪽 모두 결정론적 방출 순서의 꼬리에 덧붙여집니다. 중간에 끼워 넣으면 이후의 모든 참조가 밀려 시트가 잘못된 파트를 가리키게 됩니다

AlternateContent 앵커와 레거시 폴백

드로잉 앵커는 slicer 마크업이 진짜로 이상해지는 유일한 곳이며, 그 이상함은 의도적입니다. HotXLS는 ECMA-376 Part 3에 따라 셰이프를 mc:AlternateContent 요소 안에 방출합니다. Excel 2013 이상이 취하는 Choice 분기와, 그보다 오래된 모든 것을 위한 Fallback 분기로 구성됩니다

// XlsxSlicerAnchorPayloadXml이 구조적으로 생성하는 것:
//
//   <mc:AlternateContent>
//     <mc:Choice xmlns:sle15=".../drawing/2012/slicer" Requires="sle15">
//       <xdr:graphicFrame>
//         <a:graphicData uri=".../drawing/2010/slicer">
//           <sle:slicer xmlns:sle=".../drawing/2010/slicer" name="Region"/>
//         </a:graphicData>
//       </xdr:graphicFrame>
//     </mc:Choice>
//     <mc:Fallback><xdr:sp>...notice shape...</xdr:sp></mc:Fallback>
//   </mc:AlternateContent>

두 네임스페이스를 보세요. Requires="sle15" 토큰은 2012 slicer 네임스페이스로 해석되는데, 이것이 버전 게이트입니다. 2012 slicer를 이해하지 못하는 리더는 Choice 분기 전체를 건너뛰어야 합니다. 그러나 그 안의 graphicData/@uri와 sle:slicer 요소는 2010 네임스페이스를 사용합니다. 그래픽 프레임이 항상 써 온 드로잉 쪽 식별자이기 때문입니다. 이 둘을 뒤집으면 Excel이 불평 없이 열고는 빈 프레임으로 렌더링하는 파일이 생깁니다. 잊기 어려울 정도로 무용한 증상입니다. Fallback 분기에는 이 버전에서 테이블 slicer를 지원하지 않는다고 설명하는 텍스트 본문이 있는 평범한 사각형이 들어 있습니다. 2007 시대 리더가 컨트롤 대신 그려 넣는 것이 바로 그것이며, 오래된 Excel이 아무것도 파괴하지 않고 통합 문서를 왕복 처리할 수 있는 이유입니다. TXLSXSlicer는 지오메트리를 SetBounds(ARow1, ACol1, ARow2, ACol2)와 개별 FromRow / ToCol 속성을 통해 1-based 포함 박스로 노출하고, 평소의 셀과 함께 이동·크기 조정 동작에는 AnchorType를 제공합니다. HasAnchor가 False인 특정 경우가 하나 있습니다. 그룹 셰이프 안에 중첩된 slicer입니다. 그것들은 그룹 앵커와 함께 그대로 재생되고, HotXLS는 일부러 두 번째 앵커를 만들지 않습니다. 그렇게 하면 셰이프가 두 번 방출되기 때문입니다

기존 slicer 편집

var
  Slicer: TXLSXSlicer;
  Cache: TXLSXSlicerCache;
begin
  Slicer := Sheet.Slicers.FindByName('Region');
  if Slicer <> nil then
  begin
    Slicer.SetBounds(2, 8, 16, 11);      // 1-based, 포함
    Slicer.ShowCaption := 1;             // -1 = 속성 생략됨
    Slicer.LockedPosition := 1;
    Slicer.RowHeightEmu := 19 * XlsxEmuPerPoint;   // 241300 EMU

    Cache := Slicer.Cache;
    if Cache <> nil then
    begin
      Cache.SortOrder   := 'descending';
      Cache.CrossFilter := 'showItemsWithNoData';
    end;
  end;

  // 열 위의 마지막 뷰를 제거하면 그 캐시가 삭제됩니다
  // 그리고 예약된 #N/A 정의 이름도 함께
  Sheet.Slicers.Delete(Sheet.Slicers.IndexByName('Region'));

삼중 상태 정수 속성은 설명이 필요합니다. 모델링 사고처럼 보이지만 그렇지 않습니다. ShowCaption, LockedPosition, ColumnCount, StartItem, CustomListSort는 모두 -1을 소스 파일에 속성이 없었고 쓰기 때도 없어야 한다는 뜻으로 쓰며, 0과 1은 명시적인 값입니다. Excel은 몇몇 곳에서 속성 부재와 명시적 기본값을 다르게 취급하며, 그 구분을 보존하는 것이 건드리지 않은 저장의 바이트 diff를 깨끗하게 유지합니다. RowHeight와 ColumnWidth가 원시 문자열로 유지되는 것도 비슷한 이유입니다. Excel은 그 EMU 속성에 소수를 쓰고, 정수를 통해 다시 파싱하면 왕복에서 정밀도를 잃습니다. RowHeightEmu는 같은 저장 공간 위의 정수 편의 접근자이며, XlsxSlicerDefaultRowHeight는 241300 EMU — XlsxEmuPerPoint 비율 12700에서 19포인트 — 입니다

여기가 한계입니다

정직한 한계 두 가지. 첫째, HotXLS는 테이블 slicer를 완전히 모델링하지만 피벗 slicer 생성을 모델링하지 않습니다. 피벗 slicer가 든 통합 문서를 열고, 정체성을 읽고, 저장을 거쳐도 온전히 유지하고, 삭제할 수는 있지만, 캐시 쪽은 바이트로 재생되며 처음부터 만드는 API는 없습니다. 둘째, slicer는 그 자체로 아무것도 필터링하지 않습니다. 테이블 열 위의 UI일 뿐이고, 그것이 구동하는 보이는 행 상태는 Excel 쪽입니다. HotXLS는 컨트롤과 바인딩을 쓸 뿐 계산된 필터 결과를 쓰지 않습니다. 저장된 파일에서 행이 실제로 필터링되길 원한다면 테이블 AutoFilter도 함께 설정하세요. HotXLS가 <slicer>나 <x15:tableSlicerCache>에 모델링하지 않는 속성은 그대로 전달되므로, 더 새로운 Excel 빌드에서 온 slicer는 이동한 후에도 낯선 속성을 온전히 지닌 채 다시 직렬화됩니다. 더티 재빌드 경로를 안전하게 만들어 주는 속성이 바로 이것입니다. 모델이 파싱한 것의 슈퍼셋일 때만 모델 재빌드가 허용됩니다. ECMA-376 Part 1 §18.2.10의 GUID 태그 확장 메커니즘과 [MS-XLSX]의 slicer 파트 정의가 그 보장을 처음부터 표현 가능하게 만듭니다

테이블 slicer는 HotXLS Delphi 컴포넌트에 포함되어 있으며, 이 컴포넌트는 Excel 설치와 OLE 없이 Delphi 5부터 현재 릴리스까지의 XLSX, XLSM, 레거시 BIFF 통합 문서를 읽고 쓰고, C++Builder와 Free Pascal도 지원합니다. 전체 API 레퍼런스와 트라이얼 빌드는 HotXLS Delphi Spreadsheet Component 제품 페이지에 있습니다