Artykuł techniczny

HotXLS: zachowanie makr VBA i łączy zewnętrznych w Delphi

Rozważ zadanie, które prawie nic nie robi: otwórz miesięczny skoroszyt, wpisz dzisiejszą datę do jednej komórki, zapisz go z powrotem. Uruchom to w usłudze wystarczająco często, a reklamacja i tak przyjdzie. Makra zniknęły albo powiązane kursy walut pokazują teraz #REF!, a dział operacyjny jest przekonany, że skasował je twój kod. Nic nie skasował. Zwykle stało się to, że skoroszyt z makrami wyszedł pod zwykłą nazwą .xlsx, a Excel posłuchał reguł typów zawartości z ECMA-376: pakiet, którego typ zawartości nie deklaruje VBA, nie może wczytać projektu VBA, niezależnie od tego, czy bajty siedzą dokładnie tam, gdzie trzeba. Plik się nie zepsuł. Został przemianowany w stan, w którym Excel ma obowiązek zignorować jego część

Makra i zewnętrzne łącza między skoroszytami to dwie rzeczy, które automatyzacja gubi najniezawodniej, i to z tego samego powodu u podstaw. Oba żyją poza siatką komórek, której faktycznie dotyka edytujący kod, więc kod myślący kategoriami wierszy i kolumn upuści je, nigdy nie wydając polecenia usunięcia. HotXLS to natywna biblioteka dla Delphi i C++Builder, która czyta i zapisuje XLS oraz XLSX bez zainstalowanego Excela, i traktuje oba zasoby jako ładunek noszony świadomie, a nie dane, które przypadkiem kopiuje. Dalej opisujemy, czego każdy z nich potrzebuje od twojej ścieżki zapisu i gdzie kończą się gwarancje

Dlaczego te dwa zasoby zachowują się inaczej przy przepisaniu

Projekt VBA to jedno nieprzezroczyste binarium. W pakiecie OOXML jest to plik vbaProject.bin; w starszym pliku BIFF jest to magazyn OLE. Są dokładnie dwa sposoby, żeby go stracić: zapisujący nigdy nie kopiuje go do wyjścia albo wyjście dostaje typ pliku, który go zakazuje. Każda z tych awarii jest całkowita i cicha. Projekt albo jest obecny, albo go nie ma

Łącze zewnętrzne nie jest w ogóle blobem. To mały graf relacji: ścieżka docelowa lub URL wskazujący na inny skoroszyt, lista nazw arkuszy, które ten cel udostępnia, oraz opcjonalna pamięć podręczna wartości ostatnio widzianych w tych arkuszach, żeby Excel miał co pokazać, gdy cel jest niedostępny. Te trzy części mają przy przepisaniu różne czasy życia, a biblioteka może wiernie zachować jedne, po cichu upuszczając inne. Ta asymetria jest częścią, którą warto opisać precyzyjnie, bo nic w kodzie edytującym komórki jej nie ujawni

Diagram porównujący blob projektu VBA z trzema częściami zewnętrznego łącza skoroszytu, które HotXLS przenosi przez przepisanie w Delphi
Projekt VBA przeżywa przepisanie jako binarny ładunek na zasadzie wszystko albo nic, podczas gdy łącze zewnętrzne to mały graf, którego cel, nazwy arkuszy i wartości z pamięci podręcznej można zachować lub upuścić niezależnie

Przenoszenie projektu VBA przez przepisanie XLSX

Po stronie XLSX TXLSXWorkbook trzyma ładunek makr dosłownie. Właściwość VbaProject przechowuje surowe bajty vbaProject.bin wewnątrz AnsiString, a pusty ciąg to sposób, w jaki model mówi, że makr nie ma. Wokół niej stoją trzy operacje: HasVbaProject odpowiada, czy projekt jest obecny, ClearVbaProject usuwa go celowo, a LoadVbaProjectFromFile wstrzykuje projekt wydobyty z szablonu. To ostatnie wywołanie znaczy więcej, niż wygląda. Pozwala generowanym skoroszytom przejąć standardowy projekt makr bez przeciągania przez potok całego pliku szablonu

Diagram przepływu wywołania zapisu w Delphi, w którym rozszerzenie .xlsm wybiera typ zawartości z obsługą makr, a .xlsx sprawia, że Excel po cichu odmawia makrom
HotXLS przenosi surowe bajty vbaProject.bin przez zapis, a to rozszerzenie .xlsm wybiera wymagany przez Excela typ zawartości z obsługą makr
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');

    // Rozszerzenie .xlsm nie jest kosmetyką: wybiera ono
    // typ zawartości z obsługą makr wewnątrz pakietu.
    Book.SaveAs('monthly-report.xlsm');
  finally
    Book.Free;
  end;
end;

Linia zapisu jest miejscem, w którym rozstrzyga się cały problem. Skoroszyt trzymający projekt VBA musi zostać zapisany z semantyką obsługi makr, a HotXLS stosuje ją, gdy nazwa docelowa kończy się na .xlsm. Podaj zamiast tego .xlsx, a Excel odmówi makrom, mimo że bajty są fizycznie obecne w pakiecie i zdeserializowałyby się bez problemu. Rozszerzenie nie jest ozdobą; wybiera typ zawartości, który mówi Excelowi, że projektowi VBA wolno istnieć. Przez większość czasu wystarczy ci przeniesienie ładunku. Gdy musisz zajrzeć do środka, powiedzmy po to, by wypisać nazwy modułów do raportu audytowego, ParsedVBAProject udostępnia sparsowany model modułów, podczas gdy VbaProject pozostaje oryginalnymi, nietkniętymi bajtami

Ponowne użycie makr ze starszych skoroszytów XLS

Fasada BIFF odbija ten zestaw narzędzi z jednym dodatkowym krokiem. HasVBAProject bada wczytany plik, SaveVBAProjectToFile wypisuje magazyn projektu na dysk, a LoadVBAProjectFromFile wczytuje go z powrotem do innego skoroszytu. Objazd przez plik czyni częstą robotę modernizacyjną prostą: wyjmij makra z modelu z ery 2003 i wsadź je do świeżo wygenerowanego wyjścia XLS, bez potrzeby posiadania oryginalnego szablonu w czasie działania

var
  Src, Dst: IXLSWorkbook;   // referencje interfejsowe: bez ręcznego 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;

Pułapką jest tutaj model pamięci, a działa on odwrotnie niż w klasie XLSX. TXLSWorkbook trzymany jest przez zliczany referencyjnie interfejs IXLSWorkbook, więc nigdy nie zwalniasz go ręcznie; XLSX-owy TXLSXWorkbook to zwykły obiekt, który musisz owinąć w try..finally i zwolnić. Pomieszaj obie konwencje w jednym module, a przyjdą awarie z podwójnym zwolnieniem. Jeszcze jedna granica warta poszanowania: trzymaj wydobywanie i wstrzykiwanie w obrębie jednego formatu pliku. Magazyn projektu BIFF i OOXML-owy vbaProject.bin są kuzynami, a nie tym samym kontenerem, i potok, który musi emitować makra w obu formatach, powinien trzymać osobny szablon makr dla każdego

Łącza zewnętrzne: mapa przeżywa, wartości z pamięci podręcznej nie

Dla skoroszytów XLSX HotXLS udostępnia łącza zewnętrzne przez kolekcję ExternalLinks. Każdy TXLSXExternalLink niesie Target, czyli ścieżkę lub URL zdalnego skoroszytu, oraz listę SheetNames nazywającą arkusze, do których się odwołuje. Oba przeżywają cykl otwarcia i zapisu nietknięte, a łącze możesz też zbudować od zera:

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;

Granica leży o poziom głębiej niż lista celów. HotXLS przenosi przez pełny cykl mapę łącza, czyli cel i nazwy arkuszy, ale nie parsuje ani nie przepisuje buforowanych wartości komórek, które OOXML trzyma w elemencie sheetDataSet łącza. To ta pamięć podręczna pozwala Excelowi pokazać ostatnio znaną liczbę, gdy plik źródłowy jest niedostępny, a generowany skoroszyt wychodzi bez niej. Konsekwencja spada na odbiorcę, nie na ciebie. Otwórz taki plik tam, gdzie cel jest nieosiągalny, na laptopie poza VPN-em albo na przemianowanym udziale, a formuły zależne od łącza rozwiążą się do #REF! albo zawisną za monitem o aktualizację. Wypadają z tego dwie reguły. Nie obiecuj, że generowany skoroszyt wyświetli swoje powiązane zewnętrznie wartości bez połączenia. I czytaj niezerowe ExternalLinks.Count jako warunek wstępny dostawy, a nie jako funkcję: każdy cel musi być osiągalny stamtąd, gdzie plik faktycznie zostanie otwarty

Diagram tego, które części zewnętrznego łącza skoroszytu w HotXLS przeżywają przepisanie i co się dzieje, gdy brakuje buforowanych wartości spod sheetDataSet w trybie bez połączenia
HotXLS przenosi przez pełny cykl cel łącza i jego nazwy arkuszy, ale buforowane wartości komórek spod sheetDataSet nie trafiają do generowanego pliku

Co czytnik XLS zachowuje bajt w bajt

Dla struktur, których nie modeluje, strona BIFF ma inną odpowiedź: zostaw je dokładnie takimi, jakie zastałeś. Bufory i widoki tabel przestawnych (rodzina rekordów SX*), definicje QueryTable, zewnętrzne połączenia danych, widoki niestandardowe, obrazy nagłówków i rekordy motywów przechodzą przez cykl otwarcia i zapisu jako surowe bloki rekordów, niesparsowane i niezmienione. Same odwołania zewnętrzne przechodzą pełny cykl przez leżące pod spodem rekordy EXTERNSHEET i SupBook. Po stronie XLS nie ma dla nich typowanego API do tworzenia, ale istniejące łącze przeżywa edycję nietknięte

Zachowanie bajt w bajt to prawdziwa gwarancja z ostrą krawędzią. Ponieważ nic nie czyta zachowanej struktury, twoje edycje nie mogą jej uszkodzić. Z tego samego powodu nic jej też nie aktualizuje. Wstaw wiersze przez obszar, na który wskazuje zachowany bufor przestawny albo tabela zapytań, a struktura utrzyma swoje pierwotne współrzędne, podczas gdy dane pod spodem się przesuną. Plik nadal jest poprawnym XML-em albo BIFF-em; znaczenie po cichu wysunęło się z osi, i żaden błąd nie zapali się, żeby ci o tym powiedzieć. Układem, którego da się bronić, jest trzymanie generowanych edycji na arkuszach, które nie zawierają zachowanych struktur, a to ta sama dyscyplina, która chroni zablokowane i skonfigurowane do druku arkusze w naszym artykule o ochronie arkusza i ustawieniach strony

Weryfikacja pliku, który naprawdę zapisałeś

Oba tryby awarii są ciche w chwili zapisu, więc twierdzenie, które ma znaczenie, stawia się przez ponowne otwarcie wyjścia, a nie przez zaufanie kodowi, który je wyprodukował. Trzy kontrole pokrywają niemal wszystko. Otwórz plik ponownie i potwierdź, że HasVbaProject nadal zwraca prawdę wszędzie tam, gdzie makra były oczekiwane, co jednym testem łapie i upuszczony ładunek, i złe rozszerzenie. Odczytaj ExternalLinks.Count i porównaj z liczbą sprzed przepisania. Potem otwórz plik raz w Excelu z wyłączonymi makrami, bo walidacja typów zawartości w Excelu jest surowsza niż w jakiejkolwiek bibliotece, a to Excel jest programem, po którym twoi klienci ocenią plik

Nic z tego nie wymaga pełnego parsowania na wejściu. Gdy skoroszyty przychodzą masowo, a ty musisz tylko posegregować, które niosą treść objętą nadzorem, lekkie sondowanie z naszego artykułu o wypisywaniu arkuszy i lekkiej inspekcji skoroszytu pozwala kierować pliki z makrami i łączami do surowszego potoku, zanim w ogóle ruszy pierwsze przepisanie

Kilka pytań wraca wystarczająco często, by odpowiedzieć na nie wprost. HotXLS nigdy nie wykonuje makr, które zachowuje: w bibliotece nie ma środowiska uruchomieniowego VBA, tylko maszyneria do przechowywania, kopiowania, wydobywania i wstrzykiwania projektu jako danych. Na serwerze to własność bezpieczeństwa warta wypowiedzenia, bo wrogie makro przechodzące przez potok pozostaje bezwładne, dopóki desktopowy Excel nie otworzy pliku, a użytkownik nie włączy zawartości. Konwersja .xlsm na .xlsx z zachowaniem makr nie jest możliwa, i to reguła formatu, a nie ograniczenie biblioteki: typ zawartości .xlsx deklaruje skoroszyt bez makr, więc jedyne uczciwe wyniki to pozostanie przy .xlsm albo wywołanie ClearVbaProject i wysłanie pliku, który naprawdę makr nie ma. Ciche przemianowanie jest jedynym wyborem, który nie zadowala nikogo. A gdy powiązane komórki pokazują #REF! po przepisaniu, przyczyną jest omówiony wyżej brak pamięci podręcznej wartości: nowy plik niesie cel, ale nie buforowane liczby, więc Excel musi rozwiązać źródło w chwili otwarcia, a nieosiągalna albo zależna od środowiska ścieżka mu to udaremnia. Albo zagwarantuj osiągalność celu, albo wpisz obliczone wartości do komórek przed dostawą i zlikwiduj zależność całkowicie

Edytowanie cudzych skoroszytów to w większości praca nad zachowaniem rzeczy, których nie napisałeś i nie rozumiesz do końca. Opisane tutaj mechanizmy pełnego cyklu dla VBA i łączy zewnętrznych są częścią HotXLS Delphi Component dla Delphi i C++Builder, razem z właściwościami audytowymi, które pozwalają wykryć treść objętą nadzorem w chwili przybycia pliku