Technical Article

HotXLS: Запазване на VBA макроси и външни препратки в Delphi

Представете си задача, която не прави почти нищо: отваря месечна работна книга, записва днешната дата в една клетка и я записва обратно. Пуснете това през услуга достатъчно често и рано или късно ще получите оплакване. Макросите са изчезнали, или свързаните обменни курсове вече се четат като #REF!, а екипът по поддръжка е убеден, че вашият код ги е изтрил. Кодът ви не е изтрил нищо. Това, което обикновено се случва, е, че поддържаща макроси работна книга е била записана под обикновено име с разширение .xlsx, и Excel е спазил правилата за типове съдържание на ECMA-376: пакет, чийто тип съдържание не декларира VBA, не може да зареди VBA проект, независимо от това, че байтовете му са точно там. Файлът не се е повредил. Той просто е бил преименуван в състояние, в което Excel е задължен да игнорира част от него.

Макросите и външните препратки към работни книги са двете неща, които автоматизацията губи най-често, по една и съща основна причина. И двете живеят извън решетката от клетки, която редактиращият код реално докосва, така че код, който разсъждава в категориите на редове и колони, ще ги изпусне, без някога да е извиквал операция за изтриване. HotXLS е оригинална Delphi и C++Builder библиотека, която чете и записва XLS и XLSX без инсталиран Excel, и тя третира и двата актива като полезен товар, който пренася съзнателно, а не като данни, които случайно копира. Това, което следва, е информация за това от какво се нуждае всеки от тях при вашия път за запис и къде свършват гаранциите.

Защо тези два актива се държат различно при пренаписване

VBA проектът е един непрозрачен двоичен файл. В OOXML пакет това е файлът vbaProject.bin; в остарял BIFF файл това е OLE хранилище. Има точно два начина да го загубите: писателят никога да не го копира в изхода или изходният файл да получи тип, който го забранява. И в двата случая провалът е пълен и тих. Проектът или присъства, или не.

Външната препратка изобщо не е двоичен блок (blob). Тя е малък граф от връзки: целеви път или URL адрес, сочещ към друга работна книга, списъкът с имената на листовете, които тази цел излага, и незадължителен кеш от стойности, видени за последно в тези листове, така че Excel да може да покаже нещо, когато целта е офлайн. Тези три части имат различен жизнен цикъл при пренаписване, и дадена библиотека може вярно да запази едни, докато тихомълком изпуска други. Тази асиметрия е частта, за която си струва да бъдем точни, тъй като нищо в кода за редактиране на клетки няма да я прояви.

Пренасяне на VBA проект при пренаписване на XLSX

От страна на XLSX, TXLSXWorkbook запазва макро полезния товар дословно. Свойството VbaProject държи суровите байтове на vbaProject.bin вътре в AnsiString, а празният низ е начинът, по който моделът казва, че няма макроси. Около него стоят три операции: HasVbaProject отговаря дали проектът присъства, ClearVbaProject го премахва умишлено, а LoadVbaProjectFromFile инжектира проект, извлечен от шаблон. Това последно извикване струва повече, отколкото изглежда. То позволява на генерираните работни книги да приемат стандартен проект с макроси, без да влачат цял шаблон през конвейера.

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');

    // The .xlsm extension is not cosmetic: it selects the
    // macro-enabled content type inside the package.
    Book.SaveAs('monthly-report.xlsm');
  finally
    Book.Free;
  end;
end;

Редът за записване е този, около който се върти целият проблем. Работна книга, съдържаща VBA проект, трябва да бъде записана с макро-съвместима семантика, и HotXLS ги прилага, когато целевото име завършва на .xlsm. Подайте й вместо това .xlsx и Excel ще откаже макросите, въпреки че байтовете физически присъстват в пакета и биха се десериализирали успешно. Разширението не е украса; то избира типа съдържание, който казва на Excel, че е разрешено да съществува VBA проект. През повечето време ви е необходимо само да пренесете полезния товар. Когато трябва да прочетете в него, например за да изброите имената на модулите за одиторски доклад, ParsedVBAProject излага модел на анализиран модул, докато VbaProject остава с оригиналните недокоснати байтове.

Повторно използване на макроси от остарели XLS работни книги

Фасадата BIFF отразява този набор от инструменти с една допълнителна стъпка. HasVBAProject сондира зареден файл, SaveVBAProjectToFile записва съхранението на проекта на диска, а LoadVBAProjectFromFile чете проект обратно в друга работна книга. Заобикалянето през файл прави една обща задача по модернизация проста: изваждате макросите от модел от ерата на 2003 г. и ги поставяте в прясно генериран XLS изход, без да се изисква оригинален шаблон по време на изпълнение.

var
  Src, Dst: IXLSWorkbook;   // interface references: no manual 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 се държи през интерфейса с отчитане на препратките (reference-counted) IXLSWorkbook, така че никога не го освобождавате ръчно; XLSX класът TXLSXWorkbook е обикновен обект, който трябва да обградите с try..finally и да освободите. Смесете двете конвенции в един модул и ще последват сривове поради двойно освобождаване на памет. Още една граница, която си струва да се спазва: дръжте извличането и инжектирането в рамките на един и същ файлов формат. BIFF съхранението на проекти и OOXML файлът vbaProject.bin са роднини, а не един и същ контейнер, и конвейер, който трябва да генерира макроси и в двата формата, трябва да поддържа отделен шаблон за макроси за всеки от тях.

Външни препратки: картата оцелява, кешираните стойности - не

За работни книги XLSX, HotXLS излага външните препратки чрез колекцията ExternalLinks. Всяка TXLSXExternalLink носи Target, пътя или URL адреса на отдалечената работна книга, плюс списък 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! или ще спрат зад съобщение за актуализация. Така от това произтичат две правила. Не обещавайте, че генерираната работна книга ще показва своите външно свързани стойности офлайн. И четете ненулевия ExternalLinks.Count като предварително условие за доставка, а не като характеристика: всяка цел трябва да бъде достъпна оттам, където файлът автоматично ще бъде отворен.

Какво запазва XLS четецът байт по байт

За структури, които не моделира, страната BIFF има различен отговор: оставете ги точно така, както са намерени. Кешовете и изгледите на обобщените таблици (фамилията записи SX*), дефинициите на QueryTable, външните връзки към данни, персонализираните изгледи, картинките в горните колонтитули и записите на теми преминават през цикъл на отваряне и запис като необработени блокове записи, неанализирани и непроменени. Самите външни препратки преминават двупосочно през базовите записи EXTERNSHEET и SupBook. От страна на XLS няма API с типизирано създаване за тях, но съществуващата препратка оцелява при редактиране недокосната.

Запазването байт по байт е истинска гаранция с остър ръб. Тъй като нищо не чете съхранената структура, вашите редакции не могат да я повредят. По същата причина обаче и нищо не я актуализира. Вмъкнете редове през регион, към който сочи съхранен кеш на пивот или заявка за таблица, и структурата ще запази първоначалните си координати, докато данните отдолу се изместват. Файлът все още е валиден XML или BIFF; значението му тихо се е отклонило от съответствието и никаква грешка не се задейства, за да ви каже. Логичното оформление е генерираните промени да се държат на листове, които не съдържат съхранени структури - същата дисциплина, която защитава заключените и конфигурираните за печат листове в нашата статия за защита на работни листове и настройки на страници.

Верификация на файла, който действително сте записали

И двата режима на отказ са тихи по време на запис, така че същественото потвърждение се прави чрез повторно отваряне на изхода, а не чрез сляпо доверяване на кода, който го е произвел. Три проверки обхващат почти всичко. Отворете файла отново и потвърдете, че HasVbaProject все още връща true винаги, когато са се очаквали макроси, което улавя изгубен полезен товар и грешно разширение в един тест. Прочетете ExternalLinks.Count и го сравнете с броя преди пренаписването. След това отворете файла веднъж в Excel с деактивирани макроси, тъй като проверката за тип съдържание в Excel е по-строга от тази на всяка библиотека, а Excel е програмата, по която вашите клиенти ще оценяват файла.

Нищо от това не изисква пълен парс на влизане. Когато работните книги пристигат в голям обем и трябва само да прецените кои от тях съдържат регулирано съдържание, олекотеното сондиране в нашата статия за изброяване на листове и лека инспекция на работни книги ви позволява да маршрутизирате файловете с макроси и препратки в по-строг конвейер, преди да се стартира първото пренаписване.

Няколко въпроса възникват достатъчно често, за да им бъде отговорено директно. HotXLS никога не изпълнява макросите, които запазва: в библиотеката няма VBA среда за изпълнение (VBA runtime), а само механизъм за съхранение, копиране, извличане и инжектиране на проекта като данни. На сървър това е свойство на сигурността, което си струва да бъде отбелязано, тъй като зловреден макрос, преминаващ през конвейера, остава инертен, докато настолен Excel не отвори файла и потребителят не разреши съдържанието. Конвертирането на .xlsm в .xlsx със запазване на макросите не е възможно, и това е правило на формата, а не ограничение на библиотеката: типът съдържание .xlsx декларира работна книга без макроси, така че единствените честни резултати са да останете в .xlsm или да извикате ClearVbaProject и да доставите файл, който наистина няма такива. Тихото преименуване е единственият избор, който не задоволява никого. И когато свързаните клетки показват #REF! след пренаписване, причината е липсващият кеш на стойностите, обсъден по-горе: новият файл носи целта, но не и кешираните числа, така че Excel трябва да разреши източника при отваряне, а недостъпен или релативен спрямо средата път го проваля. Или гарантирайте, че целта е достъпна, или запишете изчислените стойности в клетките преди доставка и премахнете зависимостта изцяло.

Редактирането на чужди работни книги се състои предимно в запазване на неща, които не сте написали и не разбирате напълно. Функциите за двупосочен пренос на VBA и външни препратки, описани тук, се доставят с компонента HotXLS за Delphi и C++Builder, заедно с одиторските свойства, които ви позволяват да откривате регулирано съдържание в момента на пристигане на файла.