기술 문서

델파이 HotXLS로 VBA 매크로와 외부 링크 보존하기

거의 아무 일도 하지 않는 작업을 하나 생각해 보십시오. 월간 통합 문서를 열고, 오늘 날짜를 셀 하나에 쓰고, 도로 저장합니다. 이것을 서비스로 충분히 자주 돌리다 보면 그래도 불만이 도착합니다. 매크로가 사라졌거나, 링크된 환율이 이제 #REF!로 읽히고, 운영 팀은 여러분의 코드가 그것을 지웠다고 확신합니다. 코드는 아무것도 지우지 않았습니다. 대개 벌어진 일은 매크로 사용 통합 문서가 밋밋한 .xlsx 이름을 달고 나갔고, Excel이 ECMA-376 콘텐츠 형식 규칙을 따랐다는 것입니다. 콘텐츠 형식이 VBA를 선언하지 않은 패키지는 바이트가 바로 거기 앉아 있든 말든 VBA 프로젝트를 적재할 수 없습니다. 파일이 깨진 것이 아닙니다. Excel이 그 일부를 무시하도록 요구받는 상태로 이름이 바뀐 것입니다

매크로와 외부 통합 문서 링크는 자동화가 가장 어김없이 잃는 두 가지이고, 밑바탕의 이유는 같습니다. 둘 다 편집 코드가 실제로 건드리는 셀 격자 바깥에 살고 있어서, 행과 열로 생각하는 코드는 삭제를 한 번도 지시하지 않은 채 그것들을 떨어뜨립니다. HotXLS는 Excel 설치 없이 XLS와 XLSX를 읽고 쓰는 네이티브 델파이·C++Builder 라이브러리이며, 두 자산을 어쩌다 복사되는 데이터가 아니라 일부러 실어 나르는 짐으로 다룹니다. 이제부터는 각각이 여러분의 저장 경로에서 무엇을 필요로 하는지, 그리고 보장이 어디서 끝나는지입니다

다시 쓸 때 이 두 자산이 다르게 행동하는 이유

VBA 프로젝트는 불투명한 이진 덩어리 하나입니다. OOXML 패키지에서는 vbaProject.bin 파일이고, 예전 BIFF 파일에서는 OLE 저장소입니다. 그것을 잃는 방법은 정확히 둘뿐입니다. 기록기가 그것을 출력에 아예 복사하지 않거나, 출력이 그것을 금지하는 파일 유형을 받거나입니다. 어느 쪽 실패든 전면적이고 조용합니다. 프로젝트는 있거나 없거나입니다

외부 링크는 덩어리가 전혀 아닙니다. 그것은 관계의 작은 그래프입니다. 다른 통합 문서를 가리키는 대상 경로나 URL, 그 대상이 드러내는 시트 이름 목록, 그리고 대상이 오프라인일 때 Excel이 무엇인가를 보여 줄 수 있도록 그 시트들에서 마지막으로 본 값의 선택적 캐시입니다. 그 세 부분은 다시 쓸 때 서로 다른 수명을 가지며, 라이브러리는 일부를 충실히 지키면서 나머지는 조용히 떨어뜨릴 수 있습니다. 정확히 짚어 둘 만한 것이 바로 그 비대칭인데, 셀 편집 코드의 무엇도 그것을 드러내 주지 않기 때문입니다

VBA 프로젝트 덩어리와, HotXLS가 델파이의 다시 쓰기를 거쳐 실어 나르는 외부 통합 문서 링크의 세 부분을 견주어 보여 주는 그림
VBA 프로젝트는 전부 아니면 전무인 이진 짐으로 다시 쓰기를 견디는 반면, 외부 링크는 대상과 시트 이름과 캐시된 값이 서로 따로 지켜지거나 떨어질 수 있는 작은 그래프입니다

XLSX 다시 쓰기를 거쳐 VBA 프로젝트 나르기

XLSX 쪽에서 TXLSXWorkbook은 매크로 짐을 글자 그대로 지킵니다. VbaProject 속성이 날것의 vbaProject.bin 바이트를 AnsiString 안에 담고 있고, 빈 문자열이 매크로가 없다는 모델의 표현입니다. 그 둘레에는 연산 셋이 놓여 있습니다. HasVbaProject는 프로젝트가 있는지 답하고, ClearVbaProject는 일부러 그것을 없애고, LoadVbaProjectFromFile은 템플릿에서 뽑아낸 것을 주입합니다. 마지막 호출은 보이는 것보다 값어치가 큽니다. 생성된 통합 문서가 온전한 템플릿 파일을 파이프라인 내내 끌고 다니지 않고도 표준 매크로 프로젝트를 집어 들게 해 주기 때문입니다

델파이 저장 호출의 흐름도. .xlsm 확장자가 매크로 사용 콘텐츠 형식을 고르고, .xlsx는 Excel이 매크로를 조용히 거부하게 만든다
HotXLS는 날것의 vbaProject.bin 바이트를 저장 내내 실어 나르고, Excel이 요구하는 매크로 사용 콘텐츠 형식을 고르는 것은 .xlsm 확장자입니다
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Data');
    Sheet.Cells[1, 1].Value := 'Refreshed ' + DateTimeToStr(Now);

    Book.LoadVbaProjectFromFile('macros\vbaProject.bin');
    if not Book.HasVbaProject then
      raise Exception.Create('VBA payload failed to load');

    // .xlsm 확장자는 겉치레가 아닙니다. 패키지 안에서
    // 매크로 사용 콘텐츠 형식을 고르는 것이 바로 그것입니다.
    Book.SaveAs('monthly-report.xlsm');
  finally
    Book.Free;
  end;
end;

저장 줄이 문제 전체가 갈리는 지점입니다. VBA 프로젝트를 지닌 통합 문서는 매크로 사용 의미론으로 써야 하며, HotXLS는 대상 이름이 .xlsm으로 끝날 때 그 의미론을 적용합니다. 대신 .xlsx를 건네면, 바이트가 패키지 안에 물리적으로 있고 역직렬화도 잘 될 텐데도 Excel이 매크로를 거부합니다. 확장자는 장식이 아닙니다. VBA 프로젝트가 존재해도 된다고 Excel에게 알리는 콘텐츠 형식을 고르는 것이 확장자입니다. 대개는 짐을 나르기만 하면 됩니다. 감사 보고서용으로 모듈 이름을 나열하는 것처럼 그 안을 읽어야 할 때는, ParsedVBAProject가 구문 분석된 모듈 모델을 드러내는 동안 VbaProject는 손대지 않은 원본 바이트로 남습니다

예전 XLS 통합 문서의 매크로 재사용하기

BIFF 파사드는 그 연장 모음을 한 단계만 더 붙여 되비춥니다. HasVBAProject는 적재된 파일을 살피고, SaveVBAProjectToFile은 프로젝트 저장소를 디스크로 써 내고, LoadVBAProjectFromFile은 그것을 다른 통합 문서로 도로 읽어 들입니다. 파일을 거쳐 가는 이 우회로가 흔한 현대화 잡무를 단순하게 만들어 줍니다. 2003년대 모델에서 매크로를 들어내 갓 생성한 XLS 출력에 심으면 되고, 실행 시점에 원본 템플릿이 필요 없습니다

var
  Src, Dst: IXLSWorkbook;   // 인터페이스 참조: 수동 Free 불필요
begin
  Src := TXLSWorkbook.Create;
  if Src.Open('legacy-model.xls') <= 0 then
    raise Exception.Create('Cannot open legacy model');
  if Src.HasVBAProject then
    Src.SaveVBAProjectToFile('extracted-vba.bin');

  Dst := TXLSWorkbook.Create;
  Dst.Sheets.Add.Name := 'Report2026';
  Dst.LoadVBAProjectFromFile('extracted-vba.bin');
  Dst.SaveAs('report-with-macros.xls');
end;

여기서 함정은 메모리 모델이고, XLSX 클래스와는 반대로 돕니다. TXLSWorkbook은 참조 계수되는 IXLSWorkbook 인터페이스로 붙들리므로 결코 손으로 해제하지 않습니다. XLSX의 TXLSXWorkbooktry..finally로 감싸 해제해야 하는 평범한 객체입니다. 한 유닛 안에서 두 관례를 섞으면 이중 해제 충돌이 따라옵니다. 존중할 만한 경계가 하나 더 있습니다. 추출과 주입은 하나의 파일 형식 안에 머무르게 하십시오. BIFF 프로젝트 저장소와 OOXML vbaProject.bin은 사촌이지 같은 컨테이너가 아니며, 두 형식 모두로 매크로를 내보내야 하는 파이프라인은 형식마다 따로 매크로 템플릿을 두어야 합니다

외부 링크: 지도는 살아남고 캐시된 값은 살아남지 않습니다

XLSX 통합 문서에서 HotXLS는 ExternalLinks 컬렉션으로 외부 링크를 드러냅니다. 각 TXLSXExternalLink는 원격 통합 문서의 경로나 URL인 Target과, 그것이 참조하는 시트 이름을 담은 SheetNames 목록을 실어 나릅니다. 둘 다 열고 저장하는 주기를 온전히 견디며, 링크를 처음부터 지을 수도 있습니다:

var
  Link: TXLSXExternalLink;
begin
  Link := Book.ExternalLinks.Add('\\fileserver\finance\fx-rates-2026.xlsx');
  Link.SheetNames.Add('FX');

  if Book.ExternalLinks.Count > 0 then
    Writeln(Format('%d external link(s): delivery requires reachable targets',
      [Book.ExternalLinks.Count]));
end;

경계는 대상 목록보다 한 단계 더 깊은 곳에 앉아 있습니다. HotXLS는 링크 지도, 곧 대상과 시트 이름을 왕복시키지만, OOXML이 링크의 sheetDataSet 요소에 간직하는 캐시된 셀 값은 구문 분석하지도 다시 쓰지도 않습니다. 그 캐시는 원본 파일이 오프라인일 때 Excel이 마지막으로 알던 숫자를 보여 주게 해 주는 것이고, 생성된 통합 문서는 그것 없이 출하됩니다. 그 결과는 여러분이 아니라 받는 사람에게 떨어집니다. 대상에 닿을 수 없는 곳에서 그런 파일을 열면, 이를테면 VPN을 벗어난 노트북이나 이름이 바뀐 공유 폴더에서 열면, 그 링크에 의존하는 수식이 #REF!로 해결되거나 업데이트 안내 뒤에서 멈춥니다. 그래서 규칙 둘이 떨어져 나옵니다. 생성된 통합 문서가 외부에서 링크된 값을 오프라인에서 보여 주리라고 약속하지 마십시오. 그리고 0이 아닌 ExternalLinks.Count는 기능이 아니라 전달의 전제 조건으로 읽으십시오. 모든 대상이 그 파일이 실제로 열릴 곳에서 닿을 수 있어야 합니다

HotXLS 외부 통합 문서 링크의 어느 부분이 다시 쓰기를 견디는지, 그리고 sheetDataSet 뒤의 캐시된 값이 없을 때 오프라인에서 무슨 일이 벌어지는지 보여 주는 그림
HotXLS는 링크 대상과 그 시트 이름을 왕복시키지만, sheetDataSet 뒤의 캐시된 셀 값은 생성된 파일로 실려 가지 않습니다

XLS 리더가 바이트 단위로 지키는 것

모델링하지 않는 구조에 대해 BIFF 쪽은 다른 답을 내놓습니다. 찾은 그대로 두는 것입니다. 피벗 캐시와 피벗 뷰(SX* 레코드 계열), QueryTable 정의, 외부 데이터 연결, 사용자 지정 뷰, 머리글 그림, 테마 레코드가 모두 구문 분석되지도 수정되지도 않은 날것의 레코드 블록으로 열고 저장하는 주기를 지나갑니다. 외부 참조 자체는 바탕의 EXTERNSHEETSupBook 레코드를 통해 왕복합니다. XLS 쪽에는 그것을 만드는 형식 있는 API가 없지만, 이미 있는 링크는 편집을 손대지 않은 채 견딥니다

바이트 단위 보존은 날카로운 모서리가 딸린 진짜 보장입니다. 아무것도 보존된 구조를 읽지 않으므로 여러분의 편집이 그것을 망가뜨릴 수 없습니다. 같은 이유로 아무것도 그것을 갱신하지도 않습니다. 보존된 피벗 캐시나 쿼리 테이블이 가리키는 영역을 가로질러 행을 끼워 넣으면, 아래의 데이터가 움직이는 동안 그 구조는 원래 좌표를 붙들고 있습니다. 파일은 여전히 유효한 XML이나 BIFF이고, 의미만 조용히 어긋난 채 표류하며, 알려 주는 오류도 나지 않습니다. 방어할 수 있는 배치는 생성된 편집을 보존된 구조가 없는 시트에 두는 것인데, 워크시트 보호와 페이지 설정을 다룬 글에서 잠긴 시트와 인쇄가 구성된 시트를 지키는 것과 같은 규율입니다

실제로 쓴 파일 확인하기

두 실패 유형 모두 쓰는 시점에는 조용하므로, 중요한 단언은 만들어 낸 코드를 믿는 대신 출력을 다시 열어서 세우는 것입니다. 검사 셋이 거의 전부를 덮습니다. 파일을 다시 열어 매크로가 있어야 했던 곳에서 HasVbaProject가 여전히 true를 돌려주는지 확인하십시오. 이것으로 떨어진 짐과 잘못된 확장자를 한 테스트에서 잡습니다. ExternalLinks.Count를 읽어 다시 쓰기 전의 개수와 견주십시오. 그런 다음 매크로를 끈 채 Excel에서 파일을 한 번 여십시오. Excel의 콘텐츠 형식 검증이 어떤 라이브러리보다 엄격하고, 고객이 그 파일을 판단할 프로그램이 Excel이기 때문입니다

그 어느 것도 들어오는 길에 전체 구문 분석을 요구하지 않습니다. 통합 문서가 대량으로 도착하고 어느 것이 통제 대상 콘텐츠를 지녔는지 분류만 하면 될 때는, 시트 나열과 가벼운 통합 문서 검사를 다룬 글의 가벼운 탐색으로 매크로를 지녔거나 링크된 파일을 첫 다시 쓰기가 돌기도 전에 더 엄격한 파이프라인으로 돌려보낼 수 있습니다

몇 가지 질문은 곧바로 답할 만큼 자주 나옵니다. HotXLS는 자기가 지키는 매크로를 결코 실행하지 않습니다. 라이브러리에 VBA 런타임은 없고, 프로젝트를 데이터로 저장하고 복사하고 추출하고 주입하는 장치만 있습니다. 서버에서는 밝혀 둘 만한 보안 속성인데, 파이프라인을 지나가는 적대적 매크로가 데스크톱 Excel이 파일을 열고 사용자가 콘텐츠를 사용하도록 허용하기 전까지는 잠자코 있기 때문입니다. .xlsm.xlsx로 바꾸면서 매크로를 지키는 것은 불가능하며, 그것은 라이브러리의 한계가 아니라 형식의 규칙입니다. .xlsx 콘텐츠 형식은 매크로 없는 통합 문서를 선언하므로, 정직한 결말은 .xlsm으로 남거나 ClearVbaProject를 불러 정말로 매크로가 없는 파일을 출하하는 것뿐입니다. 조용한 이름 바꾸기는 아무도 만족시키지 못하는 유일한 선택입니다. 그리고 다시 쓴 뒤 링크된 셀이 #REF!를 보인다면, 원인은 위에서 말한 빠진 값 캐시입니다. 새 파일은 대상은 실어 나르지만 캐시된 숫자는 나르지 않으므로 Excel이 열 때 원본을 해결해야 하는데, 닿을 수 없거나 환경에 상대적인 경로가 그것을 무너뜨립니다. 대상이 닿을 수 있음을 보장하거나, 전달 전에 계산된 값을 셀에 써 넣어 의존성을 통째로 없애십시오

남의 통합 문서를 편집하는 일은 대체로 여러분이 쓰지 않았고 온전히 이해하지도 못하는 것을 지키는 일입니다. 여기서 설명한 VBA와 외부 링크 왕복 기능은 델파이와 C++Builder를 위한 HotXLS Delphi Component와 함께 제공되며, 파일이 도착하는 순간 통제 대상 콘텐츠를 알아채게 해 주는 감사 속성도 같이 딸려 옵니다