Техническа статия

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 проектен blob спрямо трите части на външна връзка към работна книга, които HotXLS пренася през Delphi пренаписване
VBA проект оцелява през пренаписване като all-or-nothing двоичен товар, докато външна връзка е малък граф, чиято цел, имена на листове и кеширани стойности могат да се запазят или изхвърлят независимо

Пренасяне на 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');

    // Разширението .xlsm не е декоративно: то избира
    // типа съдържание с активирани макроси вътре в пакета.
    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;   // интерфейсни препратки: без ръчно 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 като предварително условие за доставка, а не като характеристика: всяка цел трябва да бъде достъпна оттам, където файлът автоматично ще бъде отворен

Диаграма на потока на Delphi запис, при който разширението .xlsm избира типа съдържание с макроси, а .xlsx кара Excel тихо да откаже макросите
HotXLS пренася суровите vbaProject.bin байтове през записването, а разширението .xlsm е това, което избира macro-enabled content типа, изискван от Excel

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

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

Диаграма на кои части от външна връзка към работна книга в HotXLS оцеляват при пренаписване и какво става, когато кешираните стойности зад sheetDataSet липсват офлайн
HotXLS прекарва целта на връзката и нейните имена на листове в двете посоки, но кешираните клетъчни стойности зад sheetDataSet не се пренасят в генерирания файл

Запазването байт по байт е истинска гаранция с остър ръб. Тъй като нищо не чете съхранената структура, вашите редакции не могат да я повредят. По същата причина обаче и нищо не я актуализира. Вмъкнете редове през регион, към който сочи съхранен кеш на пивот или заявка за таблица, и структурата ще запази първоначалните си координати, докато данните отдолу се изместват. Файлът все още е валиден 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, заедно с одиторските свойства, които ви позволяват да откривате регулирано съдържание в момента на пристигане на файла