기술 문서

HotXLS Delphi Component: Delphi에서 charts, images, and drawing objects

워크시트 그리드 위에 떠 있는 것(차트, 로고, 도장, 설명 상자)은 무엇이든 도면 객체이며, 도면 객체는 두 가지로 정의됩니다. 무엇인지, 그리고 어디에 고정되어 있는지입니다. 사람들이 흔히 잘못 이해하는 부분은 앵커입니다. 차트는 셀 안에 살지 않습니다. 행과 열의 범위에 고정된 직사각형 안에 자리하며, 그것이 그리는 데이터는 앵커가 전혀 알지 못하는 별도의 A1 참조 집합입니다. 프레임을 옮겨도 플롯은 그대로 남습니다. 그 아래에 행을 삽입하면 프레임은 함께 아래로 밀려 내려갑니다. 이 두 좌표계를 명확히 구분하는 것이야말로 도면 코드를 올바르게 동작시키는 대부분의 요소입니다

HotXLS는 Excel 자동화 없이 XLS와 XLSX를 읽고 쓰는 네이티브 Object Pascal 라이브러리이며, 두 파일 형식이 도면을 서로 다르게 저장하기 때문에 두 개의 별도 도면 모델을 갖추고 있습니다. BIFF8 .xls 형식은 차트를 전용 시트에 두고, 떠 있는 도형은 워크시트에 부착된 OfficeArt 스트림에 둡니다. OOXML .xlsx 형식은 차트를 셀 직사각형에 고정한 채 그리드 안에 임베드할 수 있으며, 같은 종류의 떠 있는 그림과 도형도 함께 둘 수 있습니다. 객체 모델은 이 분리를 그대로 반영하며, 글로 다룰 가치가 있는 실패들은 모두 한 형식의 규칙을 다른 형식에 적용하는 데서 비롯됩니다

어느 컨테이너가 무엇을 담을 수 있는가

컨테이너 선택은 어떤 차트 코드보다도 앞서야 합니다. 사용 가능한 객체 타입이 두 형식 사이에서 다르기 때문입니다:

Delphi에서 HotXLS 그리기 컨테이너 비교 다이어그램: 레거시 XLS의 차트 시트와 OfficeArt 셰이프 대 XLSX의 임베드된 차트, 이미지, 텍스트 박스
두 파일 형식은 서로 다른 드로잉 API를 노출하므로, 차트 코드를 쓰기 전에 컨테이너를 골라야 합니다
  • XLS(BIFF8): 차트는 Sheets 컬렉션의 AddChartSheet로 생성된 전용 차트 시트에 삽니다. 그림, 텍스트 상자, 사각형, 타원, 선은 워크시트의 Shapes 컬렉션을 통해 관리되는 OfficeArt 도형입니다. 일반 워크시트 그리드 안에 차트를 임베드하는 API는 없습니다
  • XLSX(OOXML): 차트는 TXLSXWorksheet.AddChart로 워크시트에 직접 임베드하여 셀 직사각형에 고정하거나, TXLSXWorkbook.AddChartSheet로 전용 차트 시트에 배치할 수 있습니다. 이미지는 AddImageAddImageFromFile로, 떠 있는 레이블은 AddTextBox로 넣습니다

따라서 "숫자 옆에 차트가 있는 대시보드 시트"라고 표현된 요구사항은 사실 .xlsx를 위한 요구사항입니다. .xls에서는 차트를 자신만의 시트로 밀어내는 방식으로만 이를 근사할 수 있으며, 이는 사용자가 파일을 탐색하는 방식과 여러분의 코드가 동작해야 하는 방식을 모두 바꿉니다. XLS 쪽의 AddChartSheet가 반환하는 시트는 그리드가 아니라 차트 하위 스트림입니다. Cells.Item으로 여기에 쓰면 오류 없이 생성되지만 Excel이 열 때 버려지는 일관성 없는 도면 스트림이 만들어집니다. 차트는 그냥 사라지고, 빌드 로그의 어디에도 그 이유는 나오지 않습니다. 반환된 시트를 차트 전용으로 취급하면 "사라진 차트" 관련 보고 전체가 사라집니다

XLSX 워크시트에 차트 임베드하기

XLSX 경로는 여지가 있는 경로이며, 서두에서 언급한 두 좌표계가 구체적으로 드러나는 곳이기도 합니다. AddChart에 넘기는 앵커 직사각형은 워크시트의 행과 열로 표현되며 차트 프레임이 자리할 위치를 고정합니다. 계열 데이터는 시트 이름을 포함하는 절대 A1 참조로 표현됩니다. 이 둘은 독립적입니다. 프레임을 시트의 반대편 끝으로 옮겨도 여전히 같은 셀을 그립니다

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Chart: TXLSXChart;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Sales');
    Sheet.Cells[1, 1].Value := 'Region';
    Sheet.Cells[1, 2].Value := 'Revenue';
    Sheet.Cells[2, 1].Value := 'East';
    Sheet.Cells[2, 2].Value := 1184350;
    Sheet.Cells[3, 1].Value := 'Central';
    Sheet.Cells[3, 2].Value := 902210;
    Sheet.Cells[4, 1].Value := 'West';
    Sheet.Cells[4, 2].Value := 1010675;

    // 6~22행, 1~8열에 고정된 프레임
    Chart := Sheet.AddChart(xlsxChartColumn, 'Revenue by Region', 6, 1, 22, 8);
    Chart.AddSeries('Revenue', 'Sales!$A$2:$A$4', 'Sales!$B$2:$B$4');
    Chart.ValueAxisTitle := 'USD';

    Sheet.AddImageFromFile(1, 5, 'logo.png');
    Book.SaveAs('dashboard.xlsx');
  finally
    Book.Free;
  end;
end;

물리는 부분은 AddSeries에 건네는 범위 문자열입니다. 이는 호출되는 순간 포착되는 리터럴이며, 여러분이 나중에 스무 행의 데이터를 더 추가할지도 모른다는 것을 전혀 알지 못합니다. 이를 데이터를 쓴 다음에 계산한 행 수로부터 만들어야지, 그 전이어서는 안 됩니다. 분산형과 버블 차트는 같은 두 인수를 다른 의미로 오버로드합니다. 이제 카테고리 범위가 X 값을 제공하고 값 범위가 Y를 제공하며, 버블 반지름은 반환된 TXLSXChartSeriesBubbleSizeRange를 통해 설정된 세 번째 참조에서 나옵니다. 열/막대 계열을 벗어난 순간부터는 이 호출을 "카테고리, 값"이 아니라 "X, Y, 크기"로 읽으십시오

TXLSXChartType은 열, 막대, 선, 파이, 영역, 도넛, 분산, 버블, 방사형 플롯을 아우르며, 일상적인 보고에 필요한 목록을 모두 다룹니다. 주변 그리드 없이 페이지 전체를 차지하는 차트가 필요하다면 Book.AddChartSheetIsChartSheet 속성이 true인 시트를 반환합니다. 이는 레거시 차트 시트의 .xlsx 대응물이며 같은 기대를 안고 있습니다. 여기에는 셀 내용을 쓰지 마십시오

이미지는 바이트로 들어가며 크기는 EMU로 잽니다

그림을 삽입하는 오버로드는 두 가지이며, 이 둘을 혼동하는 것이 코드 리뷰에서 가장 자주 나타나는 이미지 버그입니다. AddImage(ARow, ACol, AData, AFormat)AData에 이미 인코딩된 그림 바이트, 즉 PNG, JPEG, GIF, BMP의 원시 콘텐츠를 원합니다. 여기에 파일 경로를 넘기면, 어떤 뷰어도 디코딩할 수 없는 40바이트짜리 문자열을 저장한 셈이 되며, 이는 배포 후 디버깅하고 싶지 않은 바로 그 깨진 이미지 아이콘 보고서입니다. 원본이 디스크의 파일일 때는 대신 AddImageFromFile을 호출해 라이브러리가 여러분을 위해 바이트를 읽고 형식을 분류하게 하십시오

그다음은 크기 조정입니다. DrawingML은 픽셀 단위로 재지 않습니다. English Metric Units로 재는데, 914400 EMU가 1인치이고 96 DPI에서는 9525 EMU가 1픽셀입니다. TXLSXImage 객체는 WidthEMUHeightEMU를 노출하므로, 180×60픽셀로 렌더링되어야 하는 로고는 1714500×571500 EMU가 필요합니다. 이 변환을 이름 붙은 상수에 넣고 그것을 기준으로 계산하십시오. 코드 곳곳에 흩어진 1714500 같은 매직 넘버는 읽을 수 없고, 누군가 목표 DPI를 바꾸는 순간 조용히 잘못된 값이 됩니다. 참고로 앵커의 행과 열은 EMU 계산의 0부터 시작하는 방식이 아니라 나머지 셀 API와 마찬가지로 1부터 시작합니다

HotXLS의 TXLSXWorksheet.AddChart 뒤에 있는 두 좌표계 다이어그램: 차트 프레임은 워크시트 행과 열에 고정되고, 시리즈 데이터는 절대 A1 참조를 사용
프레임은 행과 열에 고정되고 플롯은 절대 A1 참조를 읽으며, 어느 좌표계도 서로를 모릅니다

레거시 XLS 파일의 차트 시트와 도형

BIFF8 쪽에서는 더 풍부한 AddChartSheet 오버로드가 차트 타입, 축 제목, 그리고 각 레코드가 이름과 카테고리/값 범위를 문자열로 담고 있는 TXLSChartSeriesInfo 레코드의 오픈 배열을 받습니다. 떠 있는 도형은 별개의 문제입니다. 이들은 차트 시트가 아니라 데이터 워크시트 자체에, 그것의 Shapes 컬렉션을 통해 들어갑니다

var
  Book: IXLSWorkbook;
  Data, Trend: IXLSWorksheet;
  Series: array[0..0] of TXLSChartSeriesInfo;
begin
  Book := TXLSWorkbook.Create;   // 인터페이스 참조 카운트: Free하지 않음
  Data := Book.Sheets.Add;
  Data.Name := 'Data';
  Data.Cells.Item[1, 1].Value := 'Month';
  Data.Cells.Item[1, 2].Value := 'Units';
  Data.Cells.Item[2, 1].Value := 'Apr';
  Data.Cells.Item[2, 2].Value := 1530;
  Data.Cells.Item[3, 1].Value := 'May';
  Data.Cells.Item[3, 2].Value := 1721;

  Series[0].Name := 'Units';
  Series[0].Categories := 'Data!$A$2:$A$3';
  Series[0].Values := 'Data!$B$2:$B$3';
  Trend := Book.Sheets.AddChartSheet('Trend', xlsChartTypeLine,
    'Units sold', 'Month', 'Units', Series);
  // Trend는 차트 하위 스트림입니다: 여기에 셀 메서드를 호출하지 마십시오

  Data.Shapes.AddTextBox('Source: ERP nightly export', 6, 1, 8, 4);
  Data.Shapes.AddPicture('approved-stamp.bmp');
  Book.SaveAs('trend.xls');
end;

여기서 중요한 수명 관련 세부사항이 둘 있으며, 이 둘은 서로 반대 방향을 향합니다. TXLSWorkbookIXLSWorkbook 인터페이스를 통해 유지되며 참조 카운트가 매겨지므로, 직접 Free를 호출하면 이중 해제를 유발합니다. 앞 절의 TXLSXWorkbook은 평범한 객체이며 try..finally 안에서 반드시 해제해야 합니다. XLSX 쪽에서 빠진 Free를 지적하는 같은 코드 리뷰어가 XLS 쪽에서는 존재하는 것을 지적해야 하며, 이는 같은 유닛에서 두 형식을 모두 다룰 때 진짜로 걸려 넘어지기 쉬운 지점입니다. 도형 헬퍼 자체는 균일합니다. AddRectangle, AddOval, AddLine은 도면 영역을 지우는 DeleteInRange와 함께 모두 행/열 쌍으로 고정되므로, 그 위에 행을 삽입하는 템플릿은 그리드와 함께 이들도 밀어 이동시킵니다

레거시 파일에서 제자리를 지키는 속성이 하나 더 있습니다. TXLSPicture.TransparentColor는 비트맵에서 선택한 배경색을 마스킹해 지웁니다. 이는 BIFF 렌더링이 PNG 알파를 전혀 배운 적 없는 형식에서 직사각형이 아닌 도장("승인됨" 인장, 워터마크)을 그리드 위에 얹는 방법입니다. 도장이 만들어질 때 사용된 색을 설정하면 주변의 직사각형이 사라집니다

테마 색상은 BIFF8 왕복을 견디지 못합니다

OOXML 도면 채우기는 테마 색상 슬롯을 가리킬 수 있으며, 이것이 테마를 바꾸는 것만으로 .xlsx 전체를 다시 색칠하는 비용이 저렴한 이유입니다. BIFF8 도면 레코드에는 그런 슬롯이 없습니다. HotXLS가 XLS 도면에 테마 색상을 적용하면 그 색을 리터럴 RGB 값으로 해석해 저장합니다. 그것이 유래한 테마 인덱스는 파일이 기록되는 순간 사라지며, 다시 열어도 복구할 수 없습니다. 이는 특히 화이트 라벨 보고 도구, 즉 같은 생성된 문서를 여러 고객을 위해 다시 브랜딩하는 종류의 도구를 걸려 넘어지게 합니다. 저장된 .xls에서 이를 다시 읽어낼 수 있으리라 기대하는 대신, 테마-RGB 매핑을 여러분 자신의 설정에 유지하고 생성할 때마다 다시 적용하십시오

Delphi에서 HotXLS 이미지 삽입 다이어그램: AddImage는 인코딩된 바이트를 원하고 AddImageFromFile은 파일을 읽으며, 96 DPI 픽셀이 WidthEMU와 HeightEMU 값으로 변환
그림 바이트와 파일 경로는 서로 다른 오버로드에 속하고, 화면 픽셀 크기는 이미지 객체에 닿기 전에 EMU로 변환됩니다

관련된 결정이 성능 쪽에서도 나타납니다. 큰 레거시 파일에서 원하는 것이 셀 데이터뿐일 때, XLS 파사드는 _DisableGraphics를 true로 설정하면 도면 계층 파싱을 완전히 건너뛰도록 지시할 수 있으며, 이는 대량 읽기에서 실제로 시간을 절약해 줍니다. 함정은 영구적이라는 점입니다. 그런 방식으로 열린 워크북은 메모리에 OfficeArt 스트림이 없으므로, 저장하면 도면이 파일에서 사라져 버립니다. 이 플래그는 읽기 전용 분석 작업에만 아껴 사용하십시오. 더 넓은 성능 이야기는 HotXLS의 대규모 워크북 성능에 관한 노트에 있습니다

그리드가 바뀌는 동안 앵커를 안정적으로 유지하기

보고서는 생성 당시의 크기 그대로 머무는 경우가 드물며, 바로 여기서 서두에서 소개한 앵커 모델이 보답합니다. XLSX 파사드의 구조적 연산(InsertRows, DeleteRows, 그리고 열 대응 연산)은 셀과 함께 종속 계층들을 함께 이동시킵니다. 병합된 영역, 하이퍼링크, 코멘트, 고정 창, 필터 범위, 조건부 서식, 유효성 검사, 표, 정의된 이름, 그리고 이 주제와 관련해서는 이미지와 차트 앵커까지 모두 함께 이동합니다. 1행에 고정된 로고는 그 아래에 10행이 들어가도 맨 위에 그대로 남습니다. 데이터 블록 아래에 고정된 차트 프레임은 블록이 자라남에 따라 아래로 밀려 내려갑니다. 다시 쓰이지 않는 유일한 것은 삽입이 일어나기 전에 리터럴로 포착해 둔 범위 문자열입니다. 이는 라이브러리가 다시 살펴볼 이유가 없는 그저 텍스트일 뿐이기 때문입니다. 이것이 템플릿 채우기의 안전한 순서를 정해줍니다. 먼저 데이터를 쓰고 형태를 잡은 다음, 차트를 만들고 이미지를 배치하는 것을 마지막 패스로 하며, 모든 범위 문자열은 삽입 이전이 아니라 삽입 이후에 얻은 행 수로부터 유도하십시오

더 작은 도구 둘이 배치 도구 모음을 마무리합니다. XLS 쪽의 TXLSTextBox.SetArea는 기존 텍스트 상자나 자동 도형을 새로운 셀 직사각형으로 다시 고정시키며, 이는 푸터 블록이 이동할 때 이를 삭제하고 다시 만드는 것보다 낫습니다. 그리고 AddPicture의 비트맵 오버로드는 선택적인 투명도 플래그와 함께 살아 있는 TBitmap을 받으므로, 여러분의 VCL 코드가 그릴 수 있는 무엇이든(게이지, 스파크라인 스트립, 네이티브 목록에 없는 차트 타입) 임시 파일을 먼저 기록하지 않고도 곧바로 시트에 찍어 넣을 수 있습니다

차트와 이미지는 거의 항상 이미 구조가 잡힌 보고서 위에 얹는 마무리 계층이며, 이것이 사전 작업이 이들이 깔끔하게 안착할지를 결정하는 이유입니다. 차트가 참조할 데이터를 채우는 방법은 템플릿 기반 보고서 생성에서 다루며, 앵커 아래의 그리드를 안정적으로 유지하는 방법은 병합된 셀과 레이아웃 제어의 주제입니다. 전체 클래스 및 메서드 문서는 HotXLS Delphi Component 제품 페이지에 있습니다