שינוי שם של הפניה קשיחה לגיליון-עבודה על פני אלף תבניות דוח מאופשרות-מאקרו שולל פתיחת כל קובץ בעורך ה-VBA ידנית. HotXLS, רכיב ה-Excel הילידי של Delphi ו-C++Builder, מטפלת במקרה הזה על ידי חשיפת מקור מודול VBA כמאפיין SourceCode הניתן לעריכה ודחיסה מחדש של כל עריכה עם אלגוריתם דחיסת MS-OVBA שמיקרוסופט מגדירה עבור אחסון VBA, כותבת את התוצאה בחזרה לתוך אחסון VBA של XLS קלאסי, קובץ פרויקט VBA עצמאי, או חוברת עבודה מאופשרת-מאקרו XLSM. אין מופע Excel, אין עורך VBA, ואין מקליט מאקרו מעורב בשום מקום בנתיב הזה
למה זרם מודול VBA אינו קובץ טקסט
מודול VBA בתוך חוברת עבודה XLS או קובץ פרויקט VBA עצמאי אינו טקסט מקור שיושב בזרם וממתין להיקרא — הוא מכולה בינארית קטנה. מטמון ביצועים מקומפל בא קודם, הבייטים ש-Office משתמשת בהם כדי לדלג על קימפול-מחדש של המודול בטעינה כשהמטמון עדיין תואם את גרסת המארח, וטקסט המקור בפועל בא אחריו, עובר דרך סכימת דחיסה קניינית ש-MS-OVBA מגדירה ספציפית עבור אחסון VBA. הסכימה הזו אינה zip, אינה deflate, ואינה שום דבר שממשקי ה-API של דחיסת Windows מייצרים באופן ילידי, וזו בדיוק הסיבה שרוב ספריות ה-Excel של צד-שלישי יכולות לקרוא את המקור של מודול — פענוח הוא החצי הקל יותר של הבעיה — בעוד הן נעצרות לפני כתיבתו בחזרה, שכן דחיסה-מחדש היא המקום שבו סיבית שגויה בעדינות מייצרת קובץ ש-Excel מסרב לפתוח. כתבות פומביות על צד-הקריאה קיימות; מימושים בצד-כתיבה שבפועל מפעילים דחיסה-מחדש, ולא רק מפרקים מודול קיים לבדיקה, נדירים מספיק שזה נשאר אחת הפינות המתועדות-פחות של פורמטי קובץ Excel
מה מאפיין ה-SourceCode של HotXLS בפועל משנה?
HotXLS מייצגת כל מודול VBA כאובייקט TXLSVBAModule עם מאפיין SourceCode: WideString פשוט, והצבה לו ערך חדש היא בדיוק פשוטה כמו שהיא נראית: המודול מסומן מלוכלך (dirty) בזיכרון, ושום דבר לא נוגע בזרם ה-OLE הבסיסי עד שהפרויקט נשמר. הפרויקט עצמו מגיע מ-IXLSWorkbook.VBAProject על מנוע ה-XLS הקלאסי או TXLSXWorkbook.ParsedVBAProject על מנוע ה-OOXML מאופשר-המאקרו, שניהם מחזירים TXLSVBAProject שהמודולים שלו יושבים מאחורי אינדקסר Item[] מבוסס-1 ומאפיין Count, כך שעריכת אצווה על פני כל מודול בחוברת עבודה היא סתם לולאה על טווח מספרים שלמים
var
Wb: TXLSWorkbook;
Project: TXLSVBAProject;
I: Integer;
Updated: WideString;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Open('MonthlyReport.xls');
if Wb.HasVBAProject then
begin
Project := Wb.VBAProject;
for I := 1 to Project.Count do
begin
Updated := StringReplace(Project[I].SourceCode,
'ReportSheet2025', 'ReportSheet2026', [rfReplaceAll]);
if Updated <> Project[I].SourceCode then
Project[I].SourceCode := Updated; // marks the module dirty
end;
Wb.SaveAs('MonthlyReport.xls'); // recompresses on write
end;
finally
Wb.Free;
end;
end;
הלולאה הזו היא גם צורתו של מעבר ביקורת. לפני שאלף תבניות נוגעים בהן, רוב הצוותים קודם רוצים לדעת כמה מהן בפועל נושאות מאקרו ומה המאקרו הללו מפנים אליו, שזה התרחיש שמאחורי שולחן העבודה לביקורת והמרת חוברות עבודה — אותו Project.Count שמניע לולאת כתיבה-מחדש כאן הופך לספירת-מאקרו לפי-קובץ שם
בתוך מכולת הדחיסה של MS-OVBA
פורמט הדחיסה של MS-OVBA אורז בייטי מקור למה שהמפרט קורא CompressedContainer: בייט חתימה בודד, שנדרש להיות שווה 0x01, ואחריו רצף בלוקי CompressedChunk, כל אחד מכסה עד 4096 בייטים של נתונים מפוענחים. כותרת chunk בת 16 סיביות נושאת שלושה שדות — חתימה בת 3 סיביות שחייבת להיות שווה 3, שדה גודל בן 12 סיביות, וסיבית CompressedChunkFlag שמסמנת אם ה-payload של ה-chunk הוא בייטים מילוליים או רצף דחוס-טוקן. כאשר הדגל מוגדר, ה-payload הוא ריצה של קבוצות בנות שמונה טוקנים מקודמות בבייט-דגל, וכל טוקן הוא או בייט מילולי בודד או CopyToken: הפניית-חזרה היסט/אורך לתוך בייטים שכבר פוענחו מוקדם יותר באותו chunk, עם רוחב הסיביות המחולק בין היסט ואורך משתנה בהתאם לכמה עמוק לתוך ה-chunk המפענח כרגע יושב. חלק זה של MS-OVBA (§2.4.1, Compression and Decompression) הוא המקום שבו מימוש כתוב-ביד לרוב מאבד יום לטעות-אחד-בערכו בחישוב רוחב-הסיביות הזה
למה HotXLS כותבת chunks גולמיים במקום להתאים טוקנים
נתיב הכתיבה של HotXLS עוקף לגמרי את חצי-התאמת-הטוקנים של האלגוריתם הזה. כשהיא דוחסת מחדש מודול ערוך, כל chunk יוצא עם ה-CompressedChunkFlag מנוקה, כלומר ה-chunk מחזיק בייטים מילוליים ולא טוקני הפניה-חזרה — חוקי תחת MS-OVBA, שכן מכולה דחוסה מותרת להיות מורכבת כולה מ-chunks בלתי-דחוסים, וזה מסיר בדיוק את החלק באלגוריתם שהכי קשה לקבל נכון ידנית: מציאת הפניות-חזרה תקפות ואריזת זוג היסט/אורך לתוך רוחב סיביות שתלוי במיקום הנוכחי בתוך ה-chunk. הפשרה מופיעה בגודל הקובץ, לא בנכונות — זרם מודול שנכתב מחדש נוחת קרוב לגודל טקסט המקור שלו בתוספת כותרת בת שני-בייטים לכל בלוק של 4096 בייט, לא קטן יותר כפי ש-chunk דחוס-טוקן במלואו היה. כל קורא שמיישם את צד-הפענוח של המפרט, Excel כלולה, עדיין פותח את התוצאה נכון, משום ש-chunk גולמי הוא CompressedChunk תקף בדיוק כמו אחד דחוס-טוקן
מה HotXLS משאירה ללא-נגיעה כשהיא כותבת מחדש מודול
דחיסה-מחדש אף פעם לא מחליפה אלא חלק מזרם המודול. כל זרם מודול שומר את מטמון-הביצועים שלו קודם ואת המקור הדחוס שלו שני, וזרם ה-dir של הפרויקט רושם בדיוק איפה הפיצול הזה נופל עבור כל מודול ברשומת MODULEOFFSET; HotXLS קוראת את ההיסט הזה, שומרת על כל בייט לפניו בדיוק כפי שמצאה אותם, ובונה מחדש רק את המכולה הדחוסה מההיסט ואילך
טקסט המקור עצמו עובר הלוך-ושוב דרך עמוד הקוד (code page) של פרויקט ה-VBA עצמו ולא UTF-8 — אותו עמוד קוד מדור-קודם ש-Office כתבה איתו את הפרויקט מלכתחילה. עריכת SourceCode שמכניסה תווים מחוץ למאגר עמוד-הקוד הזה מוחלפת בשקט בתווי best-fit חלופיים כש-HotXLS מקדדת מחדש את המחרוזת בחזרה לבייטים, לא נדחית, כך שתו אזורי חריג שהוטל לתוך הערה או ליטרל מחרוזת הוא המקום הסביר ביותר להבחין באובדן. הפניות חיצוניות וקישורי ספרייה בתוך אותו פרויקט עוקבים אחר נתיב שימור קשור אך נפרד, מכוסה בהמאמר הנלווה על שימור קישורים חיצוניים של VBA, ושווה קריאה לפני שמעבר כתיבה-מחדש נוגע בפרויקט שמקשר החוצה לחוברות עבודה אחרות או ספריות סוג
איך אתה מחזיר את המאקרו שנכתבו מחדש לתוך חוברת עבודה?
שום דבר לא קורא לשלב הדחיסה-מחדש במפורש — הוא רץ אוטומטית ברגע שחוברת עבודה או פרויקט VBA עצמאי נשמרים. TXLSVBAProject.ApplyChanges עוברת על כל מודול, דוחסת מחדש את אלה שה-SourceCode שלהם השתנה מאז השמירה האחרונה, וכותבת מחדש רק את הזרם של המודול הזה; TXLSWorkbook.SaveAs הקלאסית, כאשר יעד השמירה שומר על הפורמט המקורי של הקובץ, ו-TXLSXWorkbook.SaveAs של OOXML עבור חבילת XLSM מאופשרת-מאקרו שתיהן קוראות לה פנימית לפני שמשהו נכתב לדיסק, ו-SaveVBAProjectToFile קוראת לאותה פונקציה כשהיעד הוא קובץ פרויקט VBA מנותק ולא חוברת עבודה מלאה
var
Wb: TXLSWorkbook;
begin
Wb := TXLSWorkbook.Create;
try
if Wb.LoadVBAProjectFromFile('LegacyMacros.ole') = 1 then
begin
Wb.VBAProject[1].SourceCode :=
StringReplace(Wb.VBAProject[1].SourceCode, 'OldServer', 'NewServer', [rfReplaceAll]);
Wb.SaveVBAProjectToFile('LegacyMacros_Patched.ole'); // ApplyChanges runs internally
end;
finally
Wb.Free;
end;
end;
var
Xlsx: TXLSXWorkbook;
Project: TXLSVBAProject;
begin
Xlsx := TXLSXWorkbook.Create;
try
Xlsx.Open('Dashboard.xlsm');
Project := Xlsx.ParsedVBAProject;
if Assigned(Project) then
begin
Project[1].SourceCode := StringReplace(Project[1].SourceCode,
'ConnStringV1', 'ConnStringV2', [rfReplaceAll]);
Xlsx.SaveAs('Dashboard.xlsm'); // SyncParsedVBAProject recompresses before the part is written
end;
finally
Xlsx.Free;
end;
end;
שלושת היעדים חולקים את אותה מכניקת SourceCode ו-ApplyChanges מתחת; ההבדל האמיתי היחיד ביניהם הוא איזו קריאת שמירה בסופו של דבר מפעילה את הדחיסה-מחדש
איפה זה עדיין נשבר
שני מצבי כשל נפוצים מספיק כדי לתכנן סביבם לפני שמעבר כתיבה-מחדש רץ מול קבצי ייצור. פרויקט VBA חתום דיגיטלית מפסיק להיות חתום תקף ברגע שהמקור שלו משתנה, שכן החתימה מכסה את תוכן הפרויקט; ל-HotXLS אין דרך לחתום-מחדש על פרויקט בשמך, ו-Excel מפילה או מסמנת את החתימה בפעם הבאה שהקובץ נפתח, כך שפרויקט מאקרו חתום זקוק לשלב חתימה-מחדש במורד הזרם אם החתימה ההיא היא משהו שזרימת העבודה שלך בפועל בודקת. מצב הכשל השני שייך לכל מי שמתפתה לממש מחדש את פורמט הדחיסה הזה מאפס במקום להשתמש בספרייה שכבר מטפלת בו: סיבית שגויה בודדת בכותרת chunk, בניבל-החתימה, שדה-הגודל, או דגל-הדחיסה, מייצרת קובץ ש-Excel מסרב לפתוח, בדרך כלל מאחורי אזהרת השחתה גנרית שלא נותנת רמז איזה בייט היה שגוי — בדיוק המחלקה של הבאג שאסטרטגיית כתיבת ה-chunk-הגולמי שתוארה קודם קיימת כדי למנוע
שום דבר מזה לא דורש הנדסה-לאחור של הפורמט כדי להשתמש בו. מפתחי Delphi ו-C++Builder מקבלים גישת קריאה וכתיבה ל-SourceCode, דחיסה-מחדש תואמת MS-OVBA, ושלושת יעדי הכתיבה-בחזרה המתוארים כאן כחלק מרכיב HotXLS הסטנדרטי, לצד שאר ה-API של חוברת העבודה XLS הקלאסית ו-OOXML שלו