إعادة تسمية مرجع ورقة عمل مكتوب بشكل ثابت عبر ألف قالب تقرير مفعّل بالماكرو تستبعد فتح كل ملف في محرر VBA يدويًا. يعالج HotXLS، مكوّن Excel الأصلي لـDelphi وC++Builder، تلك الحالة بعرض مصدر وحدة VBA كخاصية SourceCode قابلة للتحرير وإعادة ضغط كل تعديل بخوارزمية ضغط MS-OVBA التي تعرّفها Microsoft لتخزين VBA، وكتابة النتيجة مرة أخرى في تخزين VBA لـXLS الكلاسيكي، أو ملف مشروع VBA مستقل، أو مصنّف عمل XLSM مفعّل بالماكرو. لا نسخة Excel، ولا محرر VBA، ولا مسجّل ماكرو متورط في أي مكان في ذلك المسار
لماذا ليس تدفق وحدة VBA ملف نص
وحدة VBA داخل مصنّف عمل XLS أو ملف مشروع VBA مستقل ليست نص مصدر يجلس في تدفق ينتظر القراءة — إنها حاوية ثنائية صغيرة. تأتي ذاكرة تخزين مؤقتة للأداء المُترجَمة أولًا، البايتات التي يستخدمها Office لتخطي إعادة ترجمة الوحدة عند التحميل عندما لا تزال الذاكرة المؤقتة تطابق إصدار المضيف، ويتبعها النص المصدري الفعلي، يمر عبر مخطط ضغط خاص تعرّفه MS-OVBA تحديدًا لتخزين VBA. ذلك المخطط ليس zip، ولا deflate، ولا أي شيء تنتجه واجهات ضغط ويندوز البرمجية أصلًا، وهذا بالضبط سبب قدرة معظم مكتبات 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 بايت من بيانات مفكوكة الضغط. تحمل ترويسة كتلة من 16 بت ثلاثة حقول — توقيع من 3 بت يجب أن يساوي 3، وحقل حجم من 12 بت، وبت CompressedChunkFlag يُعلِّم ما إذا كانت حمولة الكتلة بايتات حرفية أو تسلسلًا مضغوطًا برموز. عندما يُضبط العلم، تكون الحمولة سلسلة من مجموعات مسبوقة ببايت علم من ثمانية رموز، وكل رمز إما بايت حرفي واحد أو CopyToken: مرجع خلفي بإزاحة/طول إلى بايتات فُكّ ضغطها بالفعل سابقًا في الكتلة نفسها، مع تحول عرض البت المقسوم بين الإزاحة والطول تبعًا لمدى بُعد موضع مفكك الضغط الحالي داخل الكتلة. هذا الجزء من MS-OVBA (§2.4.1، الضغط وفك الضغط) هو حيث يخسر تنفيذ مكتوب يدويًا يومًا كاملًا لخطأ بمقدار واحد في حساب عرض البت ذاك في أغلب الأحيان
لماذا يكتب HotXLS كتلًا خامًا بدلًا من مطابقة الرموز
يتجاوز مسار الكتابة في HotXLS نصف مطابقة الرموز من تلك الخوارزمية كليًا. عندما يعيد ضغط وحدة مُحرَّرة، تخرج كل كتلة مع مسح بت CompressedChunkFlag، ما يعني أن الكتلة تحمل بايتات حرفية بدلًا من رموز مرجع خلفي — وهذا قانوني بموجب MS-OVBA، بما أنه يُسمح لحاوية مضغوطة بأن تتكون بالكامل من كتل غير مضغوطة، ويزيل تحديدًا الجزء الأصعب من الخوارزمية للإتقان يدويًا: إيجاد مراجع خلفية صالحة وتعبئة زوج إزاحة/طول في عرض بت يعتمد على الموضع الحالي داخل الكتلة. تظهر المقايضة في حجم الملف، لا الصحة — يقع تدفق وحدة مُعاد كتابتها قريبًا من حجم نصها المصدري بالإضافة إلى ترويسة بايتين لكل كتلة 4096 بايت، لا أصغر بالطريقة التي ستكون عليها كتلة مضغوطة برموز بالكامل. كل قارئ ينفّذ جانب فك الضغط من المعيار، بما في ذلك Excel، لا يزال يفتح النتيجة بشكل صحيح، لأن كتلة خامة صالحة تمامًا كـCompressedChunk مثل واحدة مضغوطة برموز
ما الذي يتركه HotXLS دون مساس عندما يعيد كتابة وحدة
لا تستبدل إعادة الضغط سوى جزء من تدفق الوحدة أبدًا. يخزّن كل تدفق وحدة ذاكرته المؤقتة للأداء أولًا ومصدره المضغوط ثانيًا، ويسجّل تدفق dir الخاص بالمشروع بالضبط أين يقع ذلك الانقسام لكل وحدة في إدخال MODULEOFFSET؛ يقرأ HotXLS تلك الإزاحة، ويحتفظ بكل بايت قبلها تمامًا كما وجدها، ويعيد بناء الحاوية المضغوطة فقط من الإزاحة فصاعدًا
يعود النص المصدري نفسه ذهابًا وإيابًا عبر صفحة الرموز الخاصة بمشروع VBA نفسه بدلًا من UTF-8 — صفحة الرموز القديمة نفسها التي كتب بها Office المشروع أصلًا. تعديل SourceCode يُدخل محارف خارج مستودع صفحة الرموز تلك يُستبدَل بصمت بمحارف بديلة أفضل مطابقة عندما يعيد 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 التوقيع أو يُعلِّمه في المرة التالية التي يُفتح فيها الملف، لذا فإن مشروع ماكرو موقَّعًا يحتاج خطوة إعادة توقيع لاحقة إذا كان ذلك التوقيع شيئًا يتحقق منه سير عملك فعليًا. نمط الفشل الثاني ينتمي إلى أي شخص يُغرى بإعادة تنفيذ صيغة الضغط هذه من الصفر بدلًا من استخدام مكتبة تعالجها بالفعل: بت واحد خاطئ في ترويسة كتلة، في خانة التوقيع، أو حقل الحجم، أو علم الضغط، ينتج ملفًا يرفض Excel فتحه، عادة خلف تحذير تلف عام لا يعطي أي دليل عن أي بايت كان خاطئًا — بالضبط الفئة من الأخطاء التي وُجدت استراتيجية كتابة الكتلة الخامة الموصوفة أعلاه لتجنبها
لا شيء من هذا يتطلب هندسة عكسية للصيغة لاستخدامه. يحصل مطورو Delphi وC++Builder على وصول قراءة وكتابة لـSourceCode، وإعادة ضغط متوافقة مع MS-OVBA، وكل وجهات الكتابة الثلاث الموصوفة هنا كجزء من مكوّن HotXLS القياسي، إلى جانب بقية واجهته البرمجية لمصنّفات عمل XLS الكلاسيكية وOOXML