مشكلة بقاء EXCEL.EXE عند التعامل مع Excel من C# ── أنماط تحرير مراجع COM وقرار الاستبدال
· آخر تحديث: · غو كومورا · Excel, C#, COM, .NET, .NET Framework, Office, صيانة الأصول القديمة, الاستشارات التقنية
«عندما بنيتُ ميزة لإخراج تقرير عبر Excel، وجدتُ صفّاً طويلاً من EXCEL.EXE في مدير المهامّ» أو «أنهيتُ التطبيق، لكن عند محاولة فتح الملفّ التالية، ظهرت رسالة ‹مستخدَم من عمليّة أخرى›» أو «تراكمت مئات عمليّات Excel على خادم الدفعة الليليّة (batch)، فاستنزفت الذاكرة وتوقّف كلّ شيء». كلّ من كتب شيفرة لتشغيل Excel عبر Microsoft.Office.Interop.Excel من C# يصطدم بهذه الظاهرة تقريباً مرّة واحدة على الأقلّ. ويصلنا نحن أيضاً بانتظام استفسار من نوع «أستدعي Quit() لكنّ Excel لا ينتهي».
المزعج في هذه المشكلة أنّها تبدو وكأنّها «تحدث أحياناً فقط». تختفي في جهاز التطوير لكنّها تبقى في الإنتاج، وتبقى عند التشغيل التصحيحيّ (debug) لكنّها تختفي عند الإصدار (release) ── وبما أنّ شرط إعادة الإنتاج متذبذب، يميل الحلّ العرضيّ Process.Kill إلى التسلّل إلى شيفرة الإنتاج. لكنّ السبب ليس حظّاً، بل يمكن تفسيره تماماً بآليّة عدّ مراجع COM وآليّة RCW (Runtime Callable Wrapper) في .NET. في هذا المقال، سنُنظّم من منظور عمليّ: آليّة البقاء، والمطبّ النمطيّ «قاعدة النقطتين»، وتصنيف نمطَي التحرير وتوصية شركتنا، والطريقة الصحيحة لقتل العمليّة كملاذ أخير، وصولاً إلى قرار الاستبدال بمكتبات Open XML.
1. الخلاصة أوّلاً
- بقاء EXCEL.EXE ليس خللاً برمجيّاً، بل لأنّ جانب .NET ما زال ممسكاً بمرجع COM.
Quit()ليس أكثر من طلب بمعنى «ينتهي عندما تُحرَّر جميع المراجع»، وطالما بقي مرجع، ينتظر Excel بأمانة. - يتعامل .NET مع كائنات COM عبر غلاف يُسمّى RCW (Runtime Callable Wrapper)، ويستمرّ في الاحتفاظ بمرجع للكائن حتّى يُجمَع RCW بواسطة GC (أو يُحرَّر صراحةً).1
- ربط نقطتين أو أكثر متتاليتين، كما في
book.Worksheets[1].Range["A1"]، يُولِّد RCW لكائنات وسيطة دون أن توضَع في أيّ متغيّر، فيحدث عدم تحرير. يُعرَف هذا اصطلاحاً بـ«قاعدة النقطتين»، وهو الفاعل الرئيسيّ لهذه المشكلة. - يوجد اتّجاهان للتحرير: (أ) تطبيق
Marshal.ReleaseComObjectعلى كلّ كائنات COM بانضباط، و(ب) حصر المراجع في متغيّرات وترك جمعها لـ GC (GC.Collect+WaitForPendingFinalizers). تُصنِّف الوثائق الرسميّةReleaseComObjectبأنّه «يُستخدَم فقط عند الضرورة المطلقة».2 - توصية شركتنا هي جعل نمط GC الذي يعزل المعالجة في تابع واحد هو الأساس، وإدخال غلاف صغير مُنظَّم عبر
usingللتحرير فقط عند الحاجة إلى التحكّم بترتيب التحرير (الفصل 4). - كضمان لحالات البقاء رغم كلّ ذلك، نأخذ مقبض النافذة من
Application.Hwndمسبقاً، ونحدِّد PID عبرGetWindowThreadProcessIdثمّ نُنفِّذ Kill. لا تُستخدَم طريقة التخمين عبر الفرق بين قائمة العمليّات قبل التشغيل وبعده، لأنّها قد تتسبّب في حادثة تشمل ملفّ Excel مفتوحاً لدى المستخدم.34 - أساساً، أتمتة Office على جانب الخادم أو في بيئة غير مأهولة غير مدعومة من Microsoft.5 إن كان الأمر توليد تقارير غير مأهول، ففكِّر أوّلاً في الاستبدال بـ Open XML SDK / ClosedXML اللتين لا تشغّلان Excel (الفصلان 6-7).67
2. لماذا تبقى EXCEL.EXE ── عدّ المراجع وRCW
أوّلاً، المبدأ من جانب COM. يُدار عمر كائن COM بعدّ المراجع (reference counting)، ولا ينتهي خادم أتمتة Excel (EXCEL.EXE) حتّى تُعاد جميع المراجع الممنوحة لعملاء خارجيّين. وحتّى في زمن VB6، كان نسيان استدعاء Release يترك العمليّة باقيةً بالطريقة نفسها (لمراجعة COM، انظر «ما هو COM / ActiveX / OCX»).
ثانياً، جانب .NET. لا تلمس شيفرة C# كائن COM مباشرةً، بل تعمل عبر وكيل (proxy) يُنشئه CLR يُسمّى RCW. يُنشَأ RCW واحد داخل العمليّة لكلّ كائن COM، ويخزِّن مؤشِّر واجهة COM مؤقّتاً، ويُحرِّر مرجع كائن COM عندما يُجمَع هو نفسه بواسطة GC.1 بعبارة أخرى، استُبدِلت إدارة العمر من طريقة «عدّ المراجع بنفسك» إلى طريقة «تركها لـ GC».
بتركيب هاتين النقطتين، يُستنتَج مباشرةً سبب بقاء EXCEL.EXE.
| المرحلة | ما يحدث |
|---|---|
new Excel.Application() |
تبدأ EXCEL.EXE بالتشغيل، ويُنشأ RCW لـ Application |
| عمليّات الخلايا والحفظ وغيرها | يتزايد RCW مع كلّ كائن يُلمَس: Workbook، وWorksheet، وRange… |
excel.Quit() |
مجرّد إخبار Excel بأنّه «يجوز الإنهاء». لا ينقص أيّ مرجع ممّا يمسكه RCW |
| الخروج من التابع | تختفي إشارة .NET إلى RCW، لكنّ RCW نفسه ما زال حيّاً في الكومة |
| GC (في وقت ما) | يُجمَع RCW، وعندها فقط يُعاد مرجع COM ← تنتهي EXCEL.EXE |
هناك نقطتان أساسيّتان. أوّلاً، Quit() ليس تحريراً. طالما بقي مرجع، ينتظر Excel. من الطبيعيّ أن ينقطع المرجع وينتهي Excel أيضاً عندما تنتهي عمليّة التطبيق تماماً، لكن في أشكال يبقى فيها العمليّة الأمّ حيّة، كالتطبيقات المقيمة أو تطبيقات الويب، لا يأتي ذلك «الوقت ما». ثانياً، توقيت التحرير غير محدَّد لأنّه يعتمد على GC. إن كانت هناك سعة في الذاكرة، فقد لا يعمل GC لعشرات الدقائق، وخلال ذلك تبقى EXCEL.EXE كعمليّة زومبي. عدم إمكانيّة إعادة الإنتاج من نوع «تبقى أحياناً» أو «تبقى في الإنتاج فقط» ليس إلّا تذبذب توقيت GC ظاهراً كما هو.
علماً بأنّ Excel المُشغَّل بـ Visible = false لا يملك نافذة، فحتّى إن بقي فلن يراه المستخدم. النمط النموذجيّ هو ظهور الأعراض كخطأ «الملفّ مستخدَم» عند الحفظ الثاني، أو «بطء الجهاز»، ثمّ اكتشاف صفّ EXCEL.EXE في مدير المهامّ لأوّل مرّة. أوّل خطوة في التحقيق هي عدّ عددها عبر tasklist | findstr EXCEL.
3. المطبّ النمطيّ «قاعدة النقطتين» ── الكائنات الوسيطة غير المرئيّة
في شيفرة الاستفسارات من نوع «أُحرِّر كلّ المتغيّرات بعناية لكنّها تبقى»، توجد تقريباً دائماً أسطر مثل التالي.
// يبدو نظيفاً للوهلة الأولى، لكنّه يُنشئ RCW لا يمكن تحريره
excel.Workbooks.Open(path);
book.Worksheets[1].Range["A1"].Value2 = "hello";
ينشئ excel.Workbooks RCW لمجموعة Workbooks ويعيده. إن استدعيتَ .Open(...) دون استلام هذه القيمة المُعادة في متغيّر، يبقى RCW الخاصّ بـ Workbooks في الكومة ككائن مجهول «حيّ لكن لا أحد يشير إليه». وبما أنّه لا يوجد متغيّر، لا توجد وسيلة أصلاً لاستدعاء Marshal.ReleaseComObject عليه. السطر الثاني أشدّ خطورةً، إذ يُنشئ 3 كائنات RCW مجهولة في سطر واحد: Worksheets (مجموعة)، وWorksheets[1] (ورقة)، وRange["A1"] (نطاق).
القاعدة التجريبيّة لتجنّب ذلك هي «قاعدة النقطتين» المتداولة منذ القدم في مجتمع أتمتة Office. يمكن إعادة صياغتها بـ: لا تربط نقطتين أو أكثر متتاليتين على كائن COM، واستلم كلّ كائن وسيط في متغيّر مرّة واحدة.
// نُعطي اسماً (متغيّراً) لكلّ كائن وسيط
Excel.Workbooks books = excel.Workbooks;
Excel.Workbook book = books.Open(path);
Excel.Sheets sheets = book.Worksheets;
Excel.Worksheet sheet = (Excel.Worksheet)sheets[1];
Excel.Range cell = sheet.Range["A1"];
cell.Value2 = "hello";
قد يبدو هذا مطوَّلاً، لكنّه أسلوب كتابة لـ«جعل كلّ ما يجب تحريره قابلاً للتعداد بالكامل». نذكر أيضاً أنماطاً مشتقّة يسهل إغفالها.
- foreach: يُنشئ
foreach (Excel.Worksheet s in book.Worksheets)كائنات RCW للمجموعة والمعدِّد (enumerator)، وكذلك لكلّ عنصر. في اتّجاه ReleaseComObject، القاعدة المتّبعة هي استلام كلّ عنصر في متغيّر عبر حلقة for بفهرس. - القراءة والتجاهل داخل تعبير شرطيّ: يُنشَأ RCW أيضاً داخل تعبير مثل
if (excel.Workbooks.Count > 0). - معطيات تعبير مركّب: تعبير مثل
sheets.Add(After: sheets[sheets.Count])يُنشئ عدّة RCW مجهولة في سطر واحد. - الاشتراك في الأحداث: عند ربط معالِج (handler) بحدث Application أو Workbook، يحتفظ ذلك الارتباط بمرجع. ألغِ الاشتراك دائماً قبل الإنهاء.
4. تصنيف أنماط التحرير ── اتّجاه ReleaseComObject واتّجاه GC
يوجد أسلوبان لإنهاء EXCEL.EXE بشكل مضمون. كلاهما يعمل إن كُتب بشكل صحيح. المشكلة هي «هل يمكن الاستمرار في الكتابة الصحيحة؟»، وهنا يظهر الفرق من الناحية العمليّة.
4.1 (أ) اتّجاه تطبيق Marshal.ReleaseComObject بانضباط
يُنقِص Marshal.ReleaseComObject عدّاد المراجع الداخليّ لـ RCW، وعندما يصل إلى 0 يُحرِّر فوراً مرجع COM الذي يُمسِك به RCW.2 الميزة هي إمكانيّة التحرير في توقيت حتميّ دون انتظار GC. الشكل النمطيّ لتطبيقه على كلّ الكائنات هو التالي.
using Excel = Microsoft.Office.Interop.Excel;
using System.Runtime.InteropServices;
Excel.Application excel = null;
Excel.Workbooks books = null;
Excel.Workbook book = null;
Excel.Sheets sheets = null;
Excel.Worksheet sheet = null;
Excel.Range cell = null;
try
{
// لا تُستخدَم مُهيِّئات الكائن (object initializer) بالشكل new ... { DisplayAlerts = false }.
// فإن فشل استدعاء COM الخاصّ بالـ setter، يدخل excel إلى finally دون أن يكون قد أُسنِد بعد،
// فيتعذّر استدعاء Quit أو التحرير على EXCEL.EXE التي بدأت التشغيل بالفعل
excel = new Excel.Application();
excel.DisplayAlerts = false;
books = excel.Workbooks;
book = books.Open(templatePath);
sheets = book.Worksheets;
sheet = (Excel.Worksheet)sheets[1];
cell = sheet.Range["A1"];
cell.Value2 = "hello";
book.SaveAs(outputPath);
}
finally
{
// يُحرَّر بترتيب عكسيّ للإنشاء. بما أنّ Close أو Quit نفسهما قد يفشلان أيضاً كاستدعاء COM،
// نستخدم try/finally متداخلة لضمان الوصول دائماً إلى Quit والتحرير حتّى لو حدث استثناء في المنتصف
if (cell != null) Marshal.ReleaseComObject(cell);
if (sheet != null) Marshal.ReleaseComObject(sheet);
if (sheets != null) Marshal.ReleaseComObject(sheets);
try
{
if (book != null) book.Close(SaveChanges: false);
}
finally
{
if (book != null) Marshal.ReleaseComObject(book);
if (books != null) Marshal.ReleaseComObject(books);
try
{
if (excel != null) excel.Quit();
}
finally
{
if (excel != null) Marshal.ReleaseComObject(excel);
}
}
}
كما هو واضح، ضعف هذه الطريقة هو ارتفاع تكلفة الانضباط. يجب استلام كلّ كائن COM جرى لمسه في متغيّر دون استثناء، والتحرير بترتيب عكسيّ يشمل مسار الاستثناءات أيضاً. وبالأخذ في الاعتبار أنّ Close أو Quit نفسهما قد يفشلان (خطأ COM، أو مصنَّف مفصول، أو Excel لا يستجيب)، يجب أيضاً ضمان «الوصول إلى التحرير اللاحق حتّى لو فشل شيء في المنتصف» عبر try/finally متداخلة كما في المثال أعلاه ── وهذا مطلب صعب جدّاً بحسب الخبرة أن يلتزم به كلّ الأعضاء في كلّ تعديل. حتّى تعبير مركّب واحد بنقطتين يتسلّل في مكان واحد فقط يُعيد إحياء المشكلة.
والأهمّ من ذلك أنّ الوثائق الرسميّة نفسها تحذِّر من سوء الاستخدام. تنصّ مرجعيّة Marshal.ReleaseComObject على أنّه وسيلة لحالات الحاجة إلى تحرير الموارد في وقتها أو حين يكون لترتيب التحرير معنى، و«يُستخدَم فقط عند الضرورة المطلقة» (use the ReleaseComObject only if it is absolutely required).2 بما أنّ RCW آليّة تُشارَك كواحد لكلّ كائن COM داخل العمليّة، فإن كان مكان آخر في الشيفرة ما زال يستخدم RCW حرَّره مكان آخر بالفعل، يؤدّي ذلك إلى InvalidComObjectException، أو في أسوأ الحالات، إلى انتهاك وصول وتلف ذاكرة العمليّة.2 في بنية تشترك فيها عدّة وحدات داخل التطبيق في تشغيل Excel، تحدث هذه الحادثة فعليّاً.
علماً بأنّه عند تمرير نفس مؤشِّر الواجهة إلى CLR عدّة مرّات، قد يتجاوز عدّاد مراجع RCW 1، وفي تلك الحالة لا يُحرَّر باستدعاء واحد. يوجد أيضاً Marshal.FinalReleaseComObject الذي يجعل العدّاد 0 قسراً2، لكن لحظة الحاجة إلى هذا الـ API هي إشارة على عدم فهم إدارة العمر، لذا توصي شركتنا بإعادة النظر في التصميم.
تنبيه أمنيّ واحد على محور مختلف عن بقاء العمليّة. المصنَّف الذي فُتح بـ Workbooks.Open عبر أتمتة COM قد يُشغِّل VBA دون تحذير ماكرو. عيّنة هذا المقال تفترض فتح قالب موثوق يديره تطبيقك، لكن إن كان من المحتمل فتح مصنَّفات من مصدر خارجيّ كملفّات مجلّد مشترك أو ملفّات رفعها المستخدم، اضبط excel.AutomationSecurity = MsoAutomationSecurity.msoAutomationSecurityForceDisable (فضاء الأسماء Microsoft.Office.Core) قبل Open لتعطيل الماكرو قسراً.8 هذا يسدّ هجوماً من نوع «استبدال القالب يتحوّل مباشرةً إلى تنفيذ شيفرة عشوائيّة». ينطبق هذا التنبيه أيضاً على عيّنة نمط GC الموضَّحة لاحقاً.
4.2 (ب) اتّجاه حصر المراجع وتركها لـ GC ليجمعها
الأسلوب الآخر هو ترك تحرير RCW لـ GC وفق الآليّة الأصليّة، وتشغيل ذلك الـ GC في توقيت محدَّد. بما أنّ RCW يُحرِّر مرجع COM عند جمعه بواسطة GC1، فبإجراء «GC كامل + انتظار اكتمال الـ finalizer بعد خروج جميع المراجع التي تلمس Excel من النطاق (scope)»، يمكن إنهاء EXCEL.EXE دون كتابة ReleaseComObject ولو مرّة واحدة.
using System.Runtime.CompilerServices;
using Excel = Microsoft.Office.Interop.Excel;
public void ExportReport(string templatePath, string outputPath)
{
try
{
// نعزل المعالجة التي تلمس Excel بالكامل في تابع منفصل
ExportReportCore(templatePath, outputPath);
}
finally
{
// مسار الاستثناء هو الأكثر عرضة لبقاء EXCEL.EXE، لذا نُنفِّذ هذا دائماً في finally.
// بعد الخروج من التابع، لا يبقى أيّ مرجع إلى RCW في أيّ مكان
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect(); // الدورة الثانية لجمع RCW الذي فصله الـ finalizer
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void ExportReportCore(string templatePath, string outputPath)
{
var excel = new Excel.Application();
try
{
excel.DisplayAlerts = false;
Excel.Workbooks books = excel.Workbooks;
Excel.Workbook book = books.Open(templatePath);
Excel.Sheets sheets = book.Worksheets;
Excel.Worksheet sheet = (Excel.Worksheet)sheets[1];
Excel.Range cell = sheet.Range["A1"];
cell.Value2 = "hello";
book.SaveAs(outputPath);
book.Close(SaveChanges: false);
}
finally
{
excel.Quit();
}
}
شروط النجاح ثلاثة.
- عزل الشيفرة التي تلمس Excel في تابع واحد. طالما أبقى JIT المرجع حيّاً على المكدّس، لا يستطيع GC جمعه، لذا يجب استدعاء GC دائماً «خارج التابع الذي لمس Excel». الغرض من
NoInliningهو منع إبطال العزل بسبب التوسيع المضمَّن (inlining). - عدم تسريب RCW إلى حقل أو قيمة معادة (return value). إن تسرَّب ولو واحد خارج التابع، يبقى Excel طالما ظلّت تلك الإشارة حيّة.
- اجعلها ثلاثيّة
GC.Collect←GC.WaitForPendingFinalizers←GC.Collect. بما أنّ تنظيف RCW يتمّ عبر الـ finalizer، فالقاعدة المتّبعة هي دورتان: الاكتشاف في Collect الأولى، وانتظار اكتمال الـ finalizer، وجمع البقايا في Collect الثانية.
تنبيه: عند التشغيل بمُصحِّح (debugger) مرفَق، يُمدَّد عمر المتغيّر حتّى نهاية التابع، فقد لا يُجمَع RCW حتّى بهذه الطريقة. هذه هي حقيقة ظاهرة «تبقى في التصحيح لكن تختفي في الإصدار»، فأجرِ التحقّق من العمل دائماً ببناء Release ودون مُصحِّح.
ميزة هذه الطريقة هي عدم حدوث تسريب حتّى مع خرق قاعدة النقطتين. يجمع GC معاً كلّ ما انقطعت إشارته، بما في ذلك RCW الوسيطة المجهولة. تتركّز نقطة المراجعة في سؤال واحد: «هل المعالجة التي تلمس Excel محصورة في هذا التابع؟»، فتنخفض تكلفة الانضباط انخفاضاً كبيراً. أمّا العيب فهو رائحة شيفرة (code smell) ناتجة عن الاستدعاء الصريح لـ GC.Collect (توقّف مؤقّت بسبب GC كامل للتطبيق بأكمله)، وخطر أن يكسرها عضو لا يعرف السبب أثناء إعادة هيكلة حسنة النيّة. اترك دائماً تعليقاً يوضِّح السبب على الثلاثيّة.
4.3 توصية شركتنا ── العزل + GC كأساس، وغلاف مُنظَّم عند الحاجة
نقارن بين الاتّجاهين من منظور عمليّ.
| (أ) اتّجاه ReleaseComObject | (ب) اتّجاه GC | |
|---|---|---|
| توقيت التحرير | حتميّ (لحظة الاستدعاء) | شبه حتميّ (لحظة ثلاثيّة GC) |
| تكلفة الانضباط | مرتفعة. يجب أن يلتزم الجميع بتحويل كلّ كائن إلى متغيّر والتحرير بترتيب عكسيّ | منخفضة. يكفي الالتزام بعزل التابع فقط |
| أعراض المبالغة | InvalidComObjectException، وانتهاك وصول2 | توقّف مؤقّت بسبب GC القسريّ |
| الاتّساق مع الإرشاد الرسميّ | «فقط عند الضرورة المطلقة»2 | يتّسق مع إدارة عمر RCW الأصليّة (تركها لـ GC)1 |
| الحالات المناسبة | عندما يكون لترتيب التحرير معنى. استخدام Excel بشكل متكرّر ودقيق في عمليّة طويلة العمر | معظم معالجات التقارير التي تكتمل بـ«فتح وكتابة وإغلاق» في مكان واحد |
توصية شركتنا هي جعل نمط العزل + GC (ب) هو الأساس. من الطبيعيّ أن تنحصر معالجة مثل إخراج التقارير في تابع واحد، وبمجرّد اتّخاذ ذلك الشكل، لا يحدث تسريب هيكليّاً. كما يُتجنَّب خطر سوء استخدام ReleaseComObject، ويتّسق ذلك مع الموقف الرسميّ.
استخدم (أ) فقط عندما تكون آنيّة التحرير وترتيبه ضروريّين فعلاً، وفي تلك الحالة، لا تسمح بكتابة ReleaseComObject الخام، بل أدخِل غلافاً صغيراً مُنظَّماً عبر using.
using System.Runtime.InteropServices;
/// <summary>غلاف (wrapper) يُحرِّر كائن COM ضمن نطاق using</summary>
public readonly struct ComScope<T> : IDisposable where T : class
{
public T Value { get; }
public ComScope(T value) => Value = value;
public void Dispose()
{
if (Value is not null && Marshal.IsComObject(Value))
Marshal.ReleaseComObject(Value);
}
}
using var books = new ComScope<Excel.Workbooks>(excel.Workbooks);
using var book = new ComScope<Excel.Workbook>(books.Value.Open(templatePath));
using var sheets = new ComScope<Excel.Sheets>(book.Value.Worksheets);
using var sheet = new ComScope<Excel.Worksheet>((Excel.Worksheet)sheets.Value[1]);
using var cell = new ComScope<Excel.Range>(sheet.Value.Range["A1"]);
cell.Value.Value2 = "hello";
book.Value.SaveAs(outputPath);
book.Value.Close(SaveChanges: false);
بما أنّ Dispose يُنفَّذ بترتيب عكسيّ لترتيب إعلان using، فإنّ «التحرير بترتيب عكسيّ للإنشاء» مضمون عبر آليّة اللغة نفسها. تزداد الكتابة بمقدار .Value، لكن يمكن ضغط الانضباط إلى قاعدة واحدة: «كائن COM يُستلَم دائماً عبر ComScope». وبالمقابل، إن كُتب تعبير مركّب مثل books.Value.Open(...).Worksheets يعود التسريب من جديد، لذا يبقى تعليم قاعدة النقطتين ضروريّاً على أيّ حال.
تنبيهان مشتركان في كلا النمطين. أوّلاً، لا تجعل مربّع حوار تأكيد الحفظ يظهر قبل Quit(). اضبط DisplayAlerts = false وClose(SaveChanges: false) صراحةً. إن تجمَّد Excel المخفيّ بانتظار مربّع حوار، لن يكتمل Quit نفسه. ثانياً، بما أنّ COM الخاصّ بـ Excel يفترض STA، لا تلمس نفس Application من عدّة خيوط. نُظِّمت العلاقة بين الخيوط وشقّة (apartment) COM في «أساسيّات STA/MTA في COM».
5. الملاذ الأخير ── تحديد PID من Hwnd والتخلّص المؤكّد
حتّى مع تنفيذ نمط التحرير بشكل صحيح، تبقى حالات مثل «لا ينتهي Excel بسبب إضافة (add-in)» أو «تبقى عمليّة واحدة حتماً في مسار استثناء شاذّ». في الدفعات (batch) غير المأهولة، تسبِّب العمليّة المتبقّية الواحدة قفل ملفّ لمهمّة اليوم التالي، لذا يستحقّ الأمر تجهيز Kill للعمليّة كضمان أخير. المشكلة هي طريقة تحديد «أيّ EXCEL.EXE يُقتَل».
الخطأ الشائع هو طريقة التحديد عبر الفرق في قائمة العمليّات قبل التشغيل وبعده. مقارنة Process.GetProcessesByName("EXCEL") قبل التشغيل وبعده، واعتبار الزيادة نسخته الخاصّة ── هذا هش أمام التزامن (concurrency). فإن فتح المستخدم Excel يدويّاً أثناء أخذ الفرق، يحدث كشف خاطئ، وإن نُفِّذت مهامّ بنفس الطريقة بالتوازي، تتبادل الخلط فيما بينها. أسوأ حادثة لهذه الطريقة هي قتل ملفّ Excel غير محفوظ يعدِّله المستخدم وضياع البيانات، وهذا فعلاً وصلنا كاستفسار من عميل.
الطريقة الصحيحة للتحديد هي الحصول على مقبض النافذة العلويّة عبر خاصّيّة Hwnd الخاصّة بكائن Application الذي شغَّلته بنفسك، والحصول على مُعرِّف العمليّة التي أنشأت تلك النافذة عبر GetWindowThreadProcessId من Win32 API.34 بما أنّ المقبض خاصّ بنسختك، لا يوجد مجال للخلط مع Excel آخر.
using System.Diagnostics;
using System.Runtime.InteropServices;
using Excel = Microsoft.Office.Interop.Excel;
internal static class NativeMethods
{
[DllImport("user32.dll")]
internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
}
public void ExportReport(string templatePath, string outputPath)
{
// باستخدام معطى out، حتّى لو طار استثناء أثناء عمليّات Excel، يبقى المقبض لدى جهة الاستدعاء،
// فنصل دائماً إلى GC وKill حتّى في مسار الاستثناء (المسار الذي يحتاج الضمان فعلاً)
Process excelProcess = null;
try
{
ExportReportCore(templatePath, outputPath, out excelProcess);
}
finally
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
if (excelProcess != null)
{
KillIfStillAlive(excelProcess); // ضمان لا يفعل شيئاً إن انتهت العمليّة بشكل طبيعيّ
excelProcess.Dispose();
}
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void ExportReportCore(string templatePath, string outputPath, out Process excelProcess)
{
excelProcess = null;
var excel = new Excel.Application();
// بعد نجاح التشغيل، نُحيط بـ try/finally مباشرةً لضمان الوصول دائماً إلى Quit()
// حتّى لو فشل الحصول على Hwnd أو فتح المقبض
try
{
// بعد Quit() تختفي النافذة ولا يمكن الحصول عليها، لذا نأخذها فور التشغيل مباشرةً.
// GetProcessById يكتفي بربط PID فقط، ومقبض العمليّة الخاصّ بنظام التشغيل
// يُفتَح بتأخير عند أوّل وصول كـ WaitForExit / Kill وما شابه. لذلك
// نلمس SafeHandle أثناء حياة Excel هنا لفتح المقبض قسراً
// (طالما بقي المقبض مفتوحاً، لا يُعاد استخدام هذا الـ PID لعمليّة أخرى)
NativeMethods.GetWindowThreadProcessId((IntPtr)excel.Hwnd, out uint pid);
excelProcess = Process.GetProcessById((int)pid);
_ = excelProcess.SafeHandle;
excel.DisplayAlerts = false;
// …… عمليّات Excel ……
}
finally
{
excel.Quit();
}
}
private void KillIfStillAlive(Process excelProcess)
{
if (!excelProcess.WaitForExit(5000)) // إن كان الأمر طبيعيّاً، تختفي خلال ثوانٍ قليلة
{
logger.LogWarning("لا تنتهي EXCEL.EXE (PID {Pid})، لذا سيُنفَّذ الإنهاء القسريّ", excelProcess.Id);
excelProcess.Kill();
}
}
نقاط الانتباه في التنفيذ.
- خذ
Hwndفور التشغيل مباشرةً. بعدQuit()تُتلَف النافذة ولا يمكن الحصول عليها. يمكن الحصول علىApplication.Hwndحتّى معVisible = false.3 - Kill هو «ضمان بعد إجراء التحرير بشكل صحيح». إن اعتمدتَ عليه من البداية، لا يُنفَّذ تنظيف ملفّات Excel المؤقّتة، ما يؤدّي إلى أمور مثل تراكم ملفّات الاستعادة التلقائيّة عند التشغيل التالي. الترتيب يجب أن يكون دائماً «التحرير ← Quit ← الانتظار ← Kill فقط إن بقيت حيّة».
- انتبه لإعادة استخدام PID. بما أنّ PID في Windows يُعاد استخدامه، فإن كان التصميم يكتفي بتذكّر رقم PID ثمّ حلّه لاحقاً، يوجد خطر قتل عمليّة غير ذات صلة إن انتهى Excel خلال ذلك وأُسنِد نفس PID إلى عمليّة أخرى. انتبه إلى أنّ مجرّد استدعاء
Process.GetProcessByIdهنا غير كافٍ. بما أنّ مقبض عمليّة نظام التشغيل يُفتَح بتأخير عند أوّل وصول كـWaitForExit/Killوما شابه، فإنّه لا يمكن منع الخلط إلّا بلمسSafeHandleصراحةً لفتح المقبض أثناء حياة Excel كما في الشيفرة أعلاه (طالما بقي المقبض مفتوحاً، لا يُعاد استخدام ذلك الـ PID). - ما يغطّيه هذا الضمان هو حالة عودة التابع مع بقاء EXCEL.EXE. إن تجمَّد استدعاء COM نفسه كـ
Workbooks.OpenأوSaveAsأوQuit(بانتظار مربّع حوار نمطيّ خفيّ أو استجابة إضافة)، لن يُصَل إلىfinally، ولن يُفعَّل هذا الـ Kill أيضاً. فكِّر في إجراء من طبقتين. أوّلاً، سدّ عامل مربّع الحوار عبرDisplayAlerts = falseوAutomationSecurity المذكور آنفاً. وفوق ذلك، في الدفعات غير المأهولة، اجعل المهمّة التي تلمس Excel عمليّة منفصلة، وضع حدّاً زمنيّاً من الخارج يمكِّن من Kill كامل (كإعداد «الوقت حتّى الإيقاف» في Task Scheduler، أو Kill للعمليّة الفرعيّة من العمليّة الأمّ). بما أنّ المهلة الزمنيّة داخل العمليّة لا يمكنها مقاطعة استدعاء COM المتجمِّد، فوضع الحدّ على مستوى العمليّة هو الأضمن. - عند تفعيل Kill، سجِّله دائماً في السجلّ (log) وراقِب تواتره. إن زاد التفعيل، فهو إشارة على تراجع (regression) في شيفرة التحرير، أو على قرار الاستبدال في الفصل 7.
6. أساساً ── أتمتة Office على جانب الخادم غير مدعومة
يمكن السيطرة على بقاء EXCEL.EXE في تطبيقات العميل تقريباً بالتقنيّات حتّى هذه النقطة. لكن، فيما يخصّ الموضع الذي تتفاقم فيه هذه المشكلة أكثر ما يكون ── أتمتة Excel على خادم أو في بيئة غير مأهولة ── يوجد موقف رسميّ ينبغي التحقّق منه قبل التقنيّة.
تنصّ Microsoft صراحةً في وثيقة بعنوان «Considerations for server-side Automation of Office» على أنّها لا توصي، ولا تدعم، أتمتة Office من تطبيقات أو مكوّنات عميل غير مأهولة وغير تفاعليّة (بما فيها ASP، وASP.NET، وDCOM، وخدمات NT).5 صُمِّم Office على افتراض وجود مستخدم تفاعليّ، وافتراضات لا تشكّل مشكلة على سطح المكتب ── كإظهار مربّع حوار تأكيد عند الخطأ وانتظار الاستجابة، ومكوّنات تفترض ملفّ تعريف المستخدم المُنفِّذ، وبنية غير قابلة لإعادة الدخول (reentry) مبنيّة على STA ── تُظهِر أنيابها كلّها على خدمات أو عمليّات عاملة (worker process) في IIS.9 استفسارات مثل «عند التشغيل من خدمة Windows، لا يعود Open في الإنتاج» أو «تجمّد بانتظار مربّع حوار مرّة في الشهر على IIS» سببها كلّها هذا التكوين غير المدعوم. بقاء EXCEL.EXE في هذا التكوين ليس مجرّد مشكلة تنظيف، بل يصبح مشكلة تشغيليّة تتراكم فيها العمليّات المتجمِّدة إلى ما لا نهاية.
ما تذكره Microsoft كبديل هو تحرير تنسيق ملفّ Open XML الذي يعالج الملفّ مباشرةً دون تثبيت Office أو تشغيله، وكذلك Microsoft Graph API الذي يعالج من جهة السحابة.9 Open XML SDK هو مكتبة من إنتاج Microsoft تستطيع قراءة وكتابة تنسيقات ملفّات Office (كـ .xlsx) المُقنَّنة كمعيار ECMA-376 / ISO/IEC 29500 عبر أصناف مُنمَّطة (typed) بقوّة، ومبنيّة فوق ZIP وXML، فلا تحتاج إلى Excel نفسه.6 تختفي معاً مشاكل بقاء العمليّة والترخيص والتكوين غير المدعوم.
لكنّ Open XML SDK مكتبة «تحرِّر تنسيق الملفّ مباشرةً»، ولا توفِّر سلوك تطبيق Excel نفسه. تنصّ اعتبارات التصميم الرسميّة صراحةً على أنّها لا توفِّر سلوكيّات التطبيق كإعادة حساب الصيغ وتحديث البيانات، ولا وظيفة التحويل إلى تنسيقات أخرى (كـ PDF).10 كما أنّ الـ API أمين لبنية تنسيق الملفّ، فحتّى كتابة خليّة واحدة بالـ SDK الخام تتطلّب فهم بنية SpreadsheetML. ما يسدّ هذه الفجوة هو ClosedXML، وهو برمجيّات مفتوحة المصدر برخصة MIT تضع فوق Open XML API واجهة حدسيّة بمفاهيم «مصنَّف وورقة وخليّة». يمكنها التعامل مع .xlsx / .xlsm دون تثبيت Excel (لا تشمل التنسيق القديم .xls).7 كتبنا بالتفصيل عن التمييز بين الاستخدامين وتصميم أسلوب القوالب في سياق إخراج التقارير في «كيفيّة بناء إخراج تقارير Excel».
علماً بأنّ Microsoft 365 لديها ترخيص لـ RPA غير المأهول (unattended license)، لكنّه يجعل التنفيذ غير المأهول ممكناً من الناحية الترخيصيّة فقط، والسلوك يبقى “AS IS” ── أي أنّ التصنيف هو «استوعِب على جانب التطبيق أيّ سلوك غير متوقّع ناتج عن استخدام خارج التصميم».9 هذه النقطة يُساء فهمها كثيراً، فانتبه إلى أنّه ليس «شراء الترخيص يجعله تكويناً مدعوماً».
7. جدول القرار ── الاستمرار في استخدام COM، أم الاستبدال بمنظومة Open XML
بناءً على ما سبق، يمكن تنظيم «الاستمرار في تشغيل Excel عبر COM، أم التخلّي عنه» في ثلاثة خيارات كالتالي.
| (1) الاستمرار في استخدام COM Interop (تغليف ومنظَّم) | (2) الاستبدال بـ Open XML SDK / ClosedXML | (3) إعادة النظر في التصميم نفسه (Graph وغيره) | |
|---|---|---|---|
| Excel نفسه | ضروريّ (والترخيص لكلّ بيئة تشغيل أيضاً) | غير ضروريّ | غير ضروريّ |
| التنفيذ غير المأهول / على الخادم | تكوين غير مدعوم5 | لا مشكلة (البديل الموصى به)9 | لا مشكلة |
| خطر بقاء العمليّة | موجود (يُدار بتقنيّات هذا المقال) | غير موجود (لا تُشغَّل عمليّة) | غير موجود |
| تنفيذ الماكرو (VBA) | ممكن | غير ممكن (والحفاظ عليه أيضاً يعتمد على المكتبة ── انظر 2 أدناه) | غير ممكن |
| إعادة حساب الصيغ / الطباعة / التحويل إلى PDF | ممكن | غير ممكن10 | متوفّر جزئيّاً في Graph |
| التفاعل مع Excel المفتوح لدى المستخدم | ممكن | غير ممكن | غير ممكن |
| التنسيق القديم .xls (BIFF) | يمكن القراءة والكتابة | غير ممكن (.xlsx / .xlsm فقط)7 | ── |
| سرعة التنفيذ / التوازي | بطيء. يلزم فصل النسخ (instances) للتوازي9 | سريع. يمكن التوازي كمكتبة اعتياديّة | يعتمد على الشبكة |
محاور القرار هي أربعة أسئلة.
- هل يلزم التفاعل مع Excel أمام المستخدم مباشرةً؟ لا يمكن بناء ميزة «الكتابة في مصنَّف مفتوح لدى المستخدم، وتسليم استمرار العمليّة» إلّا عبر COM Interop. في هذه الحالة، (1) هو الخيار الوحيد، فاستثمِر في التنظيم الموضَّح في الفصل 4. تطبيقات سطح المكتب التفاعليّة لا تندرج ضمن التكوين غير المدعوم أيضاً.
- هل تلزم ميزات تطبيق Excel نفسه (تنفيذ ماكرو، إعادة حساب، طباعة، إخراج PDF)؟ لا يمكن استبدال هذه بمنظومة Open XML.10 الجمع مع التنفيذ غير المأهول هو النمط الأصعب، ففكِّر أوّلاً فيما إذا كان بالإمكان تعديل جانب المتطلّبات (نقل منطق الماكرو إلى C#، أو كتابة قيم محسوبة مسبقاً، وغيرها). ما يجب الانتباه إليه هو «الحفاظ» على القالب المحتوي على ماكرو (.xlsm). عمليّات Open XML SDK منخفضة المستوى تحافظ عليه طالما لم تلمس جزء مشروع VBA، لكنّ مكتبات عالية المستوى مثل ClosedXML تُحمِّل المصنَّف إلى نموذج كائنات (object model) وتُعيد بناء الحزمة عند الحفظ، فقد يُفقَد مشروع VBA. عند ترحيل قالب يحتوي ماكرو إلى منظومة Open XML، اجعل التحقّق الإلزاميّ باستخدام القالب الفعليّ من أنّ «الماكرو يبقى ويعمل حتّى بعد الفتح والكتابة والحفظ»، وإن تعذَّر ضمان ذلك، أبقِ ذلك التقرير فقط على COM. يمكن استخدام حُكم «ما هو VBA» مباشرةً في التعامل مع أصول VBA.
- هل هو تنفيذ غير مأهول؟ إن كان التنفيذ من خدمة، أو Task Scheduler، أو تطبيق ويب، فالإجابة الافتراضيّة هي (2). معظم متطلّبات توليد التقارير هي مجرّد «إنشاء .xlsx بصبّ القيم والتنسيق»، وهذا يكتمل بـ ClosedXML.
- ما تنسيق الإدخال والإخراج؟ إن كان يلزم معالجة ملفّ .xls الوارد من شريك تجاريّ كما هو، لا يمكن استخدام منظومة Open XML. فكِّر فيما إذا كان يمكن إدراج تحويل إلى .xlsx عند نقطة الاستقبال.
ما تقترحه شركتنا غالباً في المشاريع الفعليّة هو التقسيم: «التوليد بـ ClosedXML، وعزل المعالجة التي تحتاج Excel نفسه فعلاً حتماً فقط في COM». نُفِّذ توليد مئات التقارير يوميّاً بـ ClosedXML على الخادم، ونُنفَّذ فقط «تحديث المصنَّف المحتوي على ماكرو» الشهريّ كمعالجة COM بضغط زرّ على سطح مكتب الموظّف المسؤول ── بهذا يختفي COM من البيئة غير المأهولة، ويندرج جزء COM المتبقّي ضمن تكوين مدعوم لأنّه تطبيق تفاعليّ. هذا نهج أكثر واقعيّة من إعادة الكتابة الشاملة، ويمكن التخلّص فيه من الأجزاء الأعلى خطراً بالترتيب.
8. نقاط الانتباه في عصر .NET (Core)
نُنظّم، في حدود ما أمكن التحقّق منه، نقاط الانتباه عند الاستمرار في تشغيل Excel عبر COM في تطبيق رُحِّل من .NET Framework إلى .NET (.NET 6/8 وغيرها).
- يبقى التشغيل التبادليّ (interop) مع COM حكراً على Windows. يعمل .NET أيضاً على Linux، لكنّ دعم التشغيل التبادليّ مع COM المدمَج مقتصر على Windows.11 اجعل الهدف صريحاً كـ
net8.0-windowsفي المشروع الذي يتضمّن تشغيل Excel، ولا تتوقّع منصّات متعدّدة. عدم القدرة على التحميل في حاوية Linux يرتبط مباشرةً بقرار الاستبدال في الفصل 7 (ClosedXML يعمل في حاوية Linux). - الأساس في طريقة الإشارة هو «مرجع COM + تضمين الأنواع التبادليّة». عند إضافة Microsoft Excel Object Library كمرجع COM في Visual Studio، يُستخدَم افتراضيّاً تضمين الأنواع التبادليّة (Embed Interop Types). بما أنّه لا يُضمَّن في تجميعتك (assembly) إلّا الأنواع المستخدَمة، لا حاجة إلى توزيع PIA (Primary Interop Assembly) في بيئة التشغيل، وتزداد المتانة أمام اختلاف إصدارات Office.12
- ما زال بإمكانك استخدام
dynamicوالمعطيات الاختياريّة. ميزات C# الموجَّهة للتشغيل التبادليّ مع Office (المعطيات المُسمّاة والاختياريّة، وتبسيط استدعاء COM عبرdynamic) مدعومة أيضاً في .NET الحاليّ.12 لكنّ الكتابة بـdynamicتجعل RCW المجهولة أصعب رؤية، لذا نوصي بالكتابة بأنواع صريحة في الشيفرة التي يُراد تنظيم تحريرها. - انتبه لواجهات برمجيّة (API) تفترض منصّة معيّنة. تحمل واجهات متعلّقة بـ COM مثل
Marshal.ReleaseComObjectسمة (attribute) خاصّة بـ Windows فقط، وعند استدعائها من مشروع متعدّد المنصّات تصبح هدفاً لتحذير المحلِّل (analyzer) CA1416. يسهل إدارتها بفصل تشغيل Excel في مشروع مستقلّ. - الاتّجاه المعاكس (استدعاء .NET من VBA) موضوع مختلف. ما زال ممكناً تكوين يعرض DLL خاصّ بـ .NET 8 كـ COM ويُستخدَم من VBA، ولخّصنا الخطوات في «كيفيّة استخدام DLL خاصّ بـ .NET 8 من VBA مع الأنواع». بقلب التكوين من «تشغيل Excel من C#» إلى «استدعاء منطق C# من ماكرو Excel»، تُترَك إدارة عمر العمليّة لـ Excel نفسه، فقد تختفي مشكلة البقاء هيكليّاً في بعض الحالات.
خلاصة القول، حتّى في عصر .NET (Core)، تبقى طريقة كتابة تشغيل Excel عبر COM ومطبّاته شبيهة تقريباً بعصر .NET Framework. ما تغيَّر هو التصريح الصريح بكونه خاصّاً بـ Windows فقط، وميل المرجع نحو تضمين الأنواع التبادليّة، أمّا نقاش RCW وأنماط التحرير (الفصول 2-5) فيبقى سارياً كما هو.
9. الخلاصة
مشكلة بقاء EXCEL.EXE لا تعود ظاهرة متروكة للحظّ إن فُهِم الجسر بين إدارتَي عمر: «يعيش COM بعدّ المراجع، ويموت RCW في .NET بواسطة GC». نُلخِّصها في قائمة تحقّق كالتالي.
Quit()ليس تحريراً. لا ينتهي Excel حتّى تُعاد جميع مراجع COM التي يُمسِك بها RCW- التعبير المركّب بنقطتين أو أكثر يُولِّد RCW مجهولاً. استلِم الكائن الوسيط في متغيّر
- اجعل التحرير أساساً «عزل التابع + ثلاثيّة GC»، وإن لزم التحكّم بالترتيب، نظِّم
ReleaseComObjectعبر غلافusing. لا تُبعثِرReleaseComObjectالخام - حدِّد نسختك فقط عبر
Application.Hwnd←GetWindowThreadProcessIdمن أجل Kill الضمانيّ، وفعِّله فقط بترتيب «Quit ← الانتظار ← عند انتهاء المهلة فقط» - أتمتة Excel على الخادم أو بشكل غير مأهول تكوين غير مدعوم. انقل توليد التقارير غير المأهول إلى ClosedXML / Open XML SDK، واعزل المعالجة التي تحتاج ماكرو أو إعادة حساب في COM ضمن بيئة تفاعليّة
إن وجدتَ صفّاً من EXCEL.EXE في مدير المهامّ، فهذا إشارة إمّا على مشكلة تقنيّة (الفصلان 4-5) أو مشكلة تكوين (الفصلان 6-7). إن تردّدتَ في تحديد أيّهما ينطبق على الشيفرة بين يديك، أو من أيّ نطاق تبدأ الاستبدال إن قرّرتَه، يمكننا المساعدة بدءاً من جرد المعالجة.
مقالات ذات صلة
- كيفيّة بناء إخراج تقارير Excel - COM/Open XML/القوالب
- ما هو VBA - القيود، والمستقبل، وحالات الاستبدال، وأنماط الترحيل الواقعيّة
- ما هو COM / ActiveX / OCX - شرح شامل للفروق والعلاقات
- دليل فحص VBA وأدوات العمل الداخليّة استعداداً لإلغاء VBScript
مجالات الاستشارة ذات الصلة
تتعامل شركة Komura Soft LLC مع التحقيق في مشاكل تطبيقات Windows بما فيها أتمتة Excel/Office (بقاء العمليّة، والتجمّد، وقفل الملفّات)، وصيانة أصول COM وتنظيمها، وتصميم ترحيل معالجة التقارير إلى مكتبات منظومة Open XML.
- الاستشارة التقنيّة ومراجعة التصميم
- الاستفادة من الأصول القائمة ودعم الترحيل
- تطوير تطبيقات Windows
- التواصل معنا
المراجع
</content>
-
Microsoft Learn، Runtime Callable Wrapper. حول إنشاء RCW واحد لكلّ كائن COM داخل العمليّة، وتخزين مؤشِّر الواجهة مؤقّتاً، وتحرير مرجع كائن COM عند جمعه بواسطة GC. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn، Marshal.ReleaseComObject(Object) Method. حول سلوك إنقاص عدّاد مراجع RCW، وخطر InvalidComObjectException وانتهاك الوصول وتلف الذاكرة عند استخدام RCW مُحرَّر بالفعل، والتصنيف كـ«يُستخدَم فقط عند الضرورة المطلقة»، والعلاقة مع FinalReleaseComObject. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn، Application.hWnd property (Excel). حول أنّ خاصّيّة Hwnd لكائن Application في Excel تُعيد مقبض النافذة العلويّة. ↩ ↩2 ↩3
-
Microsoft Learn، GetWindowThreadProcessId function (winuser.h). حول إمكانيّة الحصول على مُعرِّف الخيط ومُعرِّف العمليّة اللذين أنشآ النافذة المحدَّدة. ↩ ↩2
-
دعم Microsoft، Considerations for server-side Automation of Office. حول عدم توصية Microsoft ودعمها لأتمتة Office من تطبيقات أو مكوّنات عميل غير مأهولة وغير تفاعليّة (بما فيها ASP، وASP.NET، وDCOM، وخدمات NT). ↩ ↩2 ↩3
-
Microsoft Learn، Welcome to the Open XML SDK for Office. حول كون Open XML SDK مكتبة مبنيّة على System.IO.Packaging تعالج تنسيق ملفّات Office المعياريّ ECMA-376 / ISO/IEC 29500 عبر أصناف مُنمَّطة بقوّة. ↩ ↩2
-
GitHub، ClosedXML/ClosedXML. حول كونها مكتبة برخصة MIT توفِّر واجهة حدسيّة فوق Open XML API، وتستطيع التعامل مع ملفّات Excel 2007+ (.xlsx, .xlsm) دون تثبيت Excel. ↩ ↩2 ↩3
-
Microsoft Learn، _Application.AutomationSecurity Property (Microsoft.Office.Interop.Excel). حول وضع أمان الماكرو عند فتح ملفّ برمجيّاً، وكون الافتراضيّ عند تشغيل التطبيق هو msoAutomationSecurityLow (تفعيل كلّ الماكرو)، وإمكانيّة تعطيل كلّ الماكرو دون تحذير عبر msoAutomationSecurityForceDisable. ↩
-
Microsoft Learn، Considerations for unattended automation of Office in the Microsoft 365 for unattended RPA environment. حول مشاكل الأتمتة غير المأهولة كواجهة المستخدم التفاعليّة، وتحديد المستخدم، وبنية الخيط الواحد STA، وكون السلوك AS IS حتّى تحت الترخيص غير المأهول، وأنّ البديل الموصى به هو Microsoft Graph وتحرير تنسيق ملفّ Open XML مباشرةً. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn، Open XML SDK for Office design considerations. حول أنّ Open XML SDK ليس بديلاً لنموذج كائنات Office، ولا يوفِّر سلوكيّات التطبيق كإعادة حساب الصيغ وتحديث البيانات، ولا وظيفة التحويل إلى تنسيقات أخرى. ↩ ↩2 ↩3
-
Microsoft Learn، Native interoperability ABI support. حول اقتصار دعم نظام التشغيل التبادليّ مع COM المدمَج على Windows، ودعم COM عبر ComWrappers في .NET 5+ والتوليد المصدريّ (source generation) في .NET 8+. ↩
-
Microsoft Learn، How to access Office interop objects. حول تبسيط التشغيل التبادليّ مع Office عبر المعطيات المُسمّاة والاختياريّة وdynamic، وكون تضمين الأنواع التبادليّة (Embed Interop Types) هو السلوك الافتراضيّ بدلاً من PIA. ↩ ↩2
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
هل تعمل تطبيقات الأعمال على Windows بإصدار Arm؟ ── واقع محاكاة x64 (Prism) ومكتبات DLL وCOM الأصليّة
نجيب المطوّرين ومسؤولي الأنظمة عن سؤال «هل تعمل تطبيقات الأعمال على Windows بإصدار Arm؟». نستعرض آليّة محاكاة x64 (Prism)، والطبقات التي ...
التاريخ والوقت والمناطق الزمنية في تطبيقات الأعمال ── من فخاخ DateTime إلى مبدأ التخزين بتوقيت UTC وتصميم الاختبارات
ينزاح الوقت 9 ساعات بعد نقل الخادم ── نرتّب أسباب أعطال التاريخ والوقت انطلاقًا من خاصيّة Kind في DateTime والتحويل الضمنيّ. نشرح التمييز...
التعامل مع الدقّة العالية (DPI) في WPF ── أسباب الضبابيّة والتلطّخ رغم أنّه «يُفترَض أن يكون محصّناً ضدّ DPI» وطرق العلاج
WPF مصمَّم بحيث يعي System DPI، لكن نقل النافذة إلى شاشة بدقّة DPI مختلفة يجعل الشاشة كلّها تتلطّخ، وتصبح الصور النقطيّة ضبابيّة. نُنظّم ...
دعم الدقّة العالية (High DPI) في WinForms ── أسباب النِّيَة والانكسار على شاشات 4K ومعالجتها الواقعيّة
نرتّب هنا أسباب نِيَة وانكسار تطبيقات WinForms على شاشات 4K، انطلاقاً من مفهومَي الافتراض الوهميّ لـ DPI (DPI Virtualization) وأوضاع إدرا...
التوافق الخلفي لواجهات DLL وCOM ── جدول قرار لتحديد أيّ تغيير يكسر جهة الاستدعاء
أيّ تغيير في مكوّنات DLL أو COM يكسر جهة الاستدعاء؟ نرتّب الطبقات الثلاث للتوافق -- البايناري، والمصدري، والسلوكي -- ونقدّم جدول قرار حسب...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
دعم إعادة استخدام الأصول القديمة وترحيلها
ندعم إعادة استخدام وترحيل الأصول التي تحمل قيود COM / ActiveX / OCX أو 32bit / 64bit.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- لماذا لا تنتهي عمليّة EXCEL.EXE رغم استدعاء Quit()؟
- لأنّ Quit() ليس تحريراً، بل مجرّد طلب بمعنى «يجوز الإنهاء عندما تُحرَّر جميع المراجع». يتعامل .NET مع كائنات COM عبر غلاف (wrapper) يُسمّى RCW (Runtime Callable Wrapper)، ويستمرّ RCW في الإمساك بمرجع COM حتّى يُجمَع بواسطة GC (أو يُحرَّر صراحةً). وطالما بقي مرجع ما، ينتظر Excel بأمانة. وبما أنّ توقيت التحرير يعتمد على GC وغير محدَّد، فإنّ عدم قابليّة إعادة الإنتاج من نوع «يختفي في جهاز التطوير لكنّه يبقى في الإنتاج» يمكن تفسيرها أيضاً بتذبذب توقيت GC.
- ما هي «قاعدة النقطتين» في تشغيل Excel عبر COM؟
- قاعدة تجريبيّة تنصّ على عدم ربط نقطتين أو أكثر متتاليتين على كائن COM، واستلام كلّ كائن وسيط في متغيّر مرّة واحدة. فتعبير مركّب مثل book.Worksheets[1].Range["A1"] يُنشئ عدّة RCW مجهولة في سطر واحد: Worksheets (مجموعة)، وWorksheets[1] (ورقة)، وRange["A1"] (نطاق). وبما أنّ هذه لا تُوضَع في متغيّرات، لا توجد وسيلة لاستدعاء Marshal.ReleaseComObject عليها، فتصبح الفاعل الرئيسيّ لعدم التحرير. يجب الانتباه إلى أنّ القراءة والتجاهل داخل foreach أو تعبير شرطيّ، أو معطيات تعبير مركّب، تُولِّد أيضاً RCW مجهولة بالطريقة نفسها.
- كيف تُكتَب الشيفرة لإنهاء EXCEL.EXE بشكل مضمون؟
- الموصى به هو عزل المعالجة التي تلمس Excel بالكامل في تابع (method) واحد، وتنفيذ ثلاثيّة GC.Collect ← GC.WaitForPendingFinalizers ← GC.Collect بعد الخروج من ذلك التابع. بما أنّ RCW مصمَّم أصلاً على تحرير مرجع COM عند جمعه بواسطة GC، فإنّ هذا الشكل يجمع حتّى الـ RCW المجهولة حتّى لو خُرِقت قاعدة النقطتين. توجد أيضاً طريقة تطبيق Marshal.ReleaseComObject على كلّ الكائنات، لكنّ الوثائق الرسميّة تصنِّفها بأنّها «تُستخدَم فقط عند الضرورة المطلقة»، وسوء استخدام RCW المُحرَّر بالفعل يؤدّي إلى InvalidComObjectException أو تلف الذاكرة. أدخِل غلافاً (wrapper) منظَّماً عبر using فقط عند الحاجة إلى التحكّم بترتيب التحرير.
- هل يجوز أتمتة Excel من خادم أو دفعة (batch)؟
- تنصّ Microsoft صراحةً على أنّها لا توصي، ولا تدعم، أتمتة Office من تطبيقات أو مكوّنات عميل غير مأهولة وغير تفاعليّة (بما فيها ASP.NET، وDCOM، وخدمات NT). صُمِّم Office على افتراض وجود مستخدم تفاعليّ، ولذا يحدث على الخدمات أو IIS تجمّد بانتظار مربّع حوار أو تراكم لا نهائيّ للعمليّات. لتوليد تقارير غير مأهول، البديل الموصى به هو الاستبدال بـ Open XML SDK أو ClosedXML اللتين تعالجان الملفّ مباشرةً دون تشغيل Excel. التقسيم الواقعيّ هو عزل المعالجة التي تحتاج فعليّاً إلى ميزات Excel نفسه (تنفيذ ماكرو، إعادة حساب) فقط في COM ضمن بيئة تفاعليّة.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.
روابط عامة