Technický článek

Zachování VBA maker a externích odkazů při přepisu sešitu v Delphi

Představte si úlohu, která nedělá téměř nic: otevře měsíční workbook, zapíše do jedné buňky dnešní datum, uloží ho zpět. Pusťte to přes službu dostatečně často a stejně dorazí stížnost. Makra zmizela, nebo propojené směnné kurzy teď ukazují #REF!, a provozní tým je přesvědčen, že je váš kód smazal. Nic nesmazal. Obvykle se stalo to, že workbook s podporou maker odešel pod obyčejným názvem s příponou .xlsx, a Excel se řídil pravidly typu obsahu podle ECMA-376: balíček, jehož typ obsahu neuvádí VBA, nemůže načíst projekt VBA, bez ohledu na to, jestli tam bajty fyzicky leží. Soubor se nerozbil. Byl přejmenován do stavu, ve kterém Excel musí část jeho obsahu ignorovat

Makra a externí odkazy mezi workbooky jsou dvě věci, které automatizace ztrácí nejspolehlivěji, a to ze stejného základního důvodu. Obě žijí mimo mřížku buněk, které se editační kód skutečně dotýká, takže kód uvažující v pojmech řádků a sloupců je vyřadí, aniž by kdy vydal příkaz ke smazání. HotXLS je nativní knihovna pro Delphi a C++Builder, která čte a zapisuje XLS a XLSX bez nainstalovaného Excelu, a k oběma aktivům přistupuje jako k payloadu, který nese záměrně, ne jako k datům, která náhodou zkopírovala. Dále se podíváme na to, co každé z nich potřebuje od vaší ukládací cesty a kde končí záruky

Proč se tato dvě aktiva chovají při přepisu odlišně

Projekt VBA je jeden neprůhledný binární blok. V balíčku OOXML je to soubor vbaProject.bin; ve starším souboru BIFF je to úložiště OLE. Existují přesně dva způsoby, jak o něj přijít: writer ho nikdy nezkopíruje do výstupu, nebo výstup dostane typ souboru, který ho zakazuje. Obojí selhání je úplné a tiché. Projekt buď je přítomný, nebo není

Externí odkaz naopak žádný blob není. Je to malý graf vztahů: cílová cesta nebo URL ukazující na jiný workbook, seznam názvů listů, které tento cíl vystavuje, a volitelná mezipaměť hodnot naposledy viděných v těchto listech, aby Excel mohl něco zobrazit, když je cíl nedostupný. Tyto tři části mají při přepisu různou životnost, a knihovna může některé věrně zachovat, zatímco jiné potichu vypustí. Právě tato asymetrie stojí za to mít přesně ujasněnou, protože ji kód pro editaci buněk nikdy sám neodhalí

Srovnávací diagram blobu projektu VBA proti třem částem odkazu na externí sešit, které HotXLS nese přepisem v Delphi
VBA projekt přežije přepis jako vše-nebo-nič binární payload, zatímco externí odkaz je malý graf, jehož cíl, názvy listů i kešované hodnoty se dají zachovat či zahodit nezávisle

Přenesení projektu VBA přes přepis XLSX

Na straně XLSX drží TXLSXWorkbook payload maker doslova beze změny. Vlastnost VbaProject obsahuje syrové bajty vbaProject.bin uvnitř AnsiString, a prázdný řetězec je způsob, jakým model říká, že žádná makra nejsou. Kolem toho sedí tři operace: HasVbaProject odpoví, zda je projekt přítomný, ClearVbaProject ho záměrně odstraní a LoadVbaProjectFromFile vloží ten, který byl extrahován ze šablony. Poslední z těchto volání je cennější, než na první pohled vypadá. Umožňuje vygenerovaným workbookům převzít standardní projekt maker, aniž by bylo nutné celou šablonu tahat celým pipeline

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

    // Přípona .xlsm není kosmetická: vybírá tím
    // typ obsahu s podporou maker uvnitř balíčku.
    Book.SaveAs('monthly-report.xlsm');
  finally
    Book.Free;
  end;
end;

Řádek s uložením je místo, kde se celý problém zlomí. Workbook obsahující projekt VBA musí být zapsán se sémantikou pro podporu maker, a HotXLS ji použije, když cílový název končí na .xlsm. Dáte-li mu místo toho .xlsx, Excel makra odmítne, přestože jsou bajty v balíčku fyzicky přítomné a v pořádku by se deserializovaly. Přípona není dekorace; vybírá typ obsahu, který Excelu říká, že projekt VBA smí existovat. Většinu času potřebujete jen payload přenést. Když do něj potřebujete nahlédnout, řekněme kvůli výpisu názvů modulů pro auditní report, ParsedVBAProject vystaví rozparsovaný model modulů, zatímco VbaProject zůstává původními nedotčenými bajty

Opakované použití maker ze starších workbooků XLS

Fasáda BIFF odráží stejnou sadu nástrojů s jedním krokem navíc. HasVBAProject zjistí u načteného souboru, zda projekt existuje, SaveVBAProjectToFile zapíše úložiště projektu na disk a LoadVBAProjectFromFile ho znovu načte zpět do jiného workbooku. Tato oklika přes soubor zjednodušuje běžnou modernizační úlohu: přenést makra ze starého modelu z roku 2003 a vsadit je do čerstvě vygenerovaného výstupu XLS, aniž by byla za běhu potřeba původní šablona

var
  Src, Dst: IXLSWorkbook;   // reference přes rozhraní: bez ručního 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;

Past je tady v paměťovém modelu, a jde přesně opačným směrem než u třídy XLSX. TXLSWorkbook se drží přes referenčně počítané rozhraní IXLSWorkbook, takže ho nikdy neuvolňujete ručně; XLSX TXLSXWorkbook je naopak obyčejný objekt, který musíte obalit do try..finally a uvolnit. Smíchejte obě konvence v jedné jednotce a následují pády kvůli dvojitému uvolnění. Ještě jedna hranice, kterou stojí za to respektovat: udržujte extrakci a vkládání uvnitř jednoho formátu souboru. Úložiště projektu BIFF a OOXML vbaProject.bin jsou bratranci, ne stejný kontejner, a pipeline, která musí emitovat makra v obou formátech, by měla mít pro každý z nich vlastní šablonu maker

Externí odkazy: mapa přežije, uložené hodnoty ne

U workbooků XLSX vystavuje HotXLS externí odkazy přes kolekci ExternalLinks. Každý TXLSXExternalLink nese Target, tedy cestu nebo URL vzdáleného workbooku, a k tomu seznam SheetNames pojmenovávající listy, na které odkazuje. Obojí přežije cyklus otevření a uložení neporušené, a odkaz můžete sestavit i od nuly:

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;

Hranice leží o úroveň hlouběji než seznam cílů. HotXLS provede round-trip mapy odkazů, tedy cíle a názvů listů, ale neparsuje ani nepřepisuje uložené hodnoty buněk, které OOXML drží v elementu sheetDataSet odkazu. Právě tato mezipaměť umožňuje Excelu zobrazit naposledy známé číslo, když je zdrojový soubor nedostupný, a vygenerovaný workbook se posílá bez ní. Důsledek dopadne na příjemce, ne na vás. Otevřete-li takový soubor tam, kde je cíl nedosažitelný, notebook mimo VPN nebo přejmenované sdílení, vzorce závislé na odkazu se vyhodnotí na #REF! nebo se zaseknou za výzvou k aktualizaci. Z toho plynou dvě pravidla. Neslibujte, že vygenerovaný workbook zobrazí své externě propojené hodnoty offline. A nenulovou hodnotu ExternalLinks.Count berte jako podmínku doručení, ne jako funkci: každý cíl musí být dosažitelný odkudkoli, kde bude soubor skutečně otevřen

Vývojový diagram volání uložení v Delphi, kde přípona .xlsm vybere content type s makry a .xlsx donutí Excel potichu odmítnout makra
HotXLS přenese surové bajty vbaProject.bin skrze uložení a přípona .xlsm je tím, co vybere typ obsahu s makry, který Excel vyžaduje

Co čtečka XLS zachovává bajt po bajtu

U struktur, které nemodeluje, má strana BIFF jinou odpověď: nechat je přesně tak, jak byly nalezeny. Mezipaměti a zobrazení kontingenčních tabulek (rodina záznamů SX*), definice QueryTable, externí datová připojení, vlastní zobrazení, obrázky v záhlaví a záznamy motivů - to vše projde cyklem otevření a uložení jako syrové bloky záznamů, nerozparsované a neupravené. Samotné externí reference proběhnou round-tripem skrz podkladové záznamy EXTERNSHEET a SupBook. Na straně XLS pro ně neexistuje typované API pro vytváření, ale existující odkaz přežije editaci nedotčený

Diagram toho, které části odkazu na externí sešit HotXLS přežijí přepis, a co se stane, když chybí offline cachované hodnoty za sheetDataSet
HotXLS převede cíl odkazu i názvy jeho listů oběma směry, ale kešované hodnoty buněk za sheetDataSet se do vygenerovaného souboru nepřenáší

Zachování bajt po bajtu je skutečná záruka, ale s ostrou hranou. Protože nic zachovanou strukturu nečte, vaše úpravy ji nemohou poškodit. Ze stejného důvodu ji ale ani nic neaktualizuje. Vložte řádky přes oblast, na kterou ukazuje zachovaná mezipaměť kontingenční tabulky nebo dotazová tabulka, a struktura si podrží své původní souřadnice, zatímco data pod ní se posunou. Soubor je stále platné XML nebo BIFF; význam se ale potichu vychýlil z rovnováhy a žádná chyba vás na to neupozorní. Obhajitelné řešení je držet vygenerované úpravy na listech, které neobsahují žádné zachované struktury, což je stejná disciplína, jaká chrání zamčené a tiskově nastavené listy v našem článku o ochraně listů a nastavení stránky

Ověření souboru, který jste skutečně zapsali

Oba způsoby selhání jsou při zápisu tiché, takže na tvrzení, na kterém záleží, se přijde až opětovným otevřením výstupu, ne důvěrou v kód, který ho vyprodukoval. Téměř vše pokryjí tři kontroly. Znovu otevřete soubor a ověřte, že HasVbaProject stále vrací true všude tam, kde se makra očekávala, což jedním testem odhalí zahozený payload i špatnou příponu. Přečtěte ExternalLinks.Count a porovnejte ho s počtem před přepisem. Pak soubor jednou otevřete v Excelu s vypnutými makry, protože ověřování typu obsahu v Excelu je přísnější než u kterékoli knihovny, a právě podle Excelu budou soubor posuzovat vaši zákazníci

Nic z toho nevyžaduje na vstupu plný parsing. Když workbooky přicházejí v objemu a vy jen potřebujete roztřídit, které z nich nesou regulovaný obsah, lehké sondování popsané v našem článku o výpisu listů a lehké inspekci workbooků vám umožní nasměrovat soubory s makry a odkazy do přísnějšího pipeline dřív, než vůbec proběhne první přepis

Pár otázek se objevuje dost často na to, aby stálo za to je zodpovědět přímo. HotXLS makra, která zachovává, nikdy nespouští: v knihovně není žádný runtime VBA, jen mechanismus pro ukládání, kopírování, extrakci a vkládání projektu jako dat. Na serveru je to bezpečnostní vlastnost, kterou stojí za to zmínit, protože nepřátelské makro procházející pipeline zůstává neaktivní, dokud soubor neotevře desktopový Excel a uživatel v něm nepovolí obsah. Převést .xlsm na .xlsx a přitom si ponechat makra není možné, a je to pravidlo formátu, ne omezení knihovny: typ obsahu .xlsx deklaruje workbook bez maker, takže jedinými poctivými výsledky jsou zůstat u .xlsm, nebo zavolat ClearVbaProject a doručit soubor, který makra skutečně nemá. Tiché přejmenování je jediná volba, která neuspokojí nikoho. A když propojené buňky po přepisu ukážou #REF!, příčinou je chybějící mezipaměť hodnot popsaná výše: nový soubor nese cíl, ale ne uložená čísla, takže Excel musí zdroj vyřešit až při otevření, a nedosažitelná nebo na prostředí závislá cesta ho porazí. Buď zaručte, že cíl bude dosažitelný, nebo před doručením zapište do buněk vypočítané hodnoty a závislost zcela odstraňte

Editace cizích workbooků je většinou práce spočívající v zachování věcí, které jste nenapsali a plně jim nerozumíte. Zde popsané mechanismy round-tripu pro VBA a externí odkazy jsou součástí HotXLS Delphi Component pro Delphi a C++Builder, spolu s auditními vlastnostmi, které umožňují odhalit regulovaný obsah ve chvíli, kdy soubor dorazí