الطباعة وإخراج PDF في تطبيقات أعمال Windows ── كيفيّة الاختيار بين System.Drawing.Printing وWPF ومكتبات التقارير
· آخر تحديث: · غو كومورا · CSharp, .NET, WinForms, WPF, الطباعة, PDF, التقارير, تطوير Windows, الاستشارات التقنية
الطباعة في تطبيقات أعمال Windows ليست مسألة «ترسم شيئاً ما عبر PrintDocument وتنتهي». فواصل الصفحات لا تعمل، الهوامش تنزاح، المعاينة تختلف عن الإخراج الفعليّ، تريد PDF فقط لكن يتدخّل مربّع حوار الطباعة ── هذه المشكلات سببها غالباً البدء في التنفيذ مع سوء فهم لآليّة الطباعة نفسها.
في هذا المقال، نرتّب أربعة خيارات — System.Drawing.Printing في WinForms، وFlowDocument/FixedDocument في WPF، ومكتبات إخراج PDF، والتقارير عبر Excel/Word — من زاوية كيفيّة المفاضلة بينها حسب المتطلّبات.
1. الخلاصة أوّلاً
- بالنسبة للقوائم البسيطة أو امتداد أصول WinForms القائمة، يكفي
PrintDocument. فواصل الصفحات آليّة تستدعي حدثPrintPageتكراراً حتّى تصبحHasMorePagesقيمتهاfalse، وإغفال ذلك يمنع إخراج الصفحة الثانية وما بعدها.12 - دقّة الطباعة (DPI) شيء مختلف عن دقّة الشاشة. يمثّل
PageSettings.PrinterResolutionدقّة الطابعة، والهوامش مبنيّة على مستويين:PageSettings.HardMarginX/HardMarginY(المنطقة الفيزيائيّة غير القابلة للطباعة التي تفرضها الطابعة) وMargins(الهامش المنطقيّ الذي يحدِّده التطبيق). إعادة استخدام إحداثيّات الشاشة كما هي في الطباعة يسبِّب انزياح التخطيط بسبب اختلاف نظامَي DPI والإحداثيّات هذين.345 - في WPF، ينقسم مسار الطباعة القياسيّ في Windows إلى مسارَين: مسار طباعة GDI ومسار طباعة XPS. وتستخدم تطبيقات WPF أصلاً مسار XPS، وعند الإرسال إلى طابعة لا تدعم XPSDrv يُحوَّل الشكل تلقائيّاً إلى صيغة GDI.67
- تختلف فلسفة التصميم بين
FlowDocument(«التدفّق») وFixedDocument(«التخطيط الثابت») في مستندات WPF. يعطيFlowDocumentأولويّة لقابليّة القراءة ويُعاد تخطيطه حسب حجم النافذة والدقّة، بينما يعطيFixedDocumentأولويّة لدقّة إعادة الإنتاج على جهاز العرض والطباعة.89 - تحديد الورق وصينيّة التغذية يُنجَز عبر
PrintDialogوَPrintTicket/PrintQueueالخاصّة بـ WPF. والأسلوب المتعارف عليه هو التحقّق والدمج مقابل القدرات الفعليّة للطابعة عبرPrintQueue.MergeAndValidatePrintTicketقبل الطباعة.1011 - Microsoft Print to PDF برنامج تشغيل طباعة مبنيّ على افتراض التفاعل الحواريّ. موقعه هو طابور طباعة وحزمة برنامج تشغيل مضمَّنَين افتراضيّاً في نظام التشغيل، وبنيته الأساسيّة تتضمّن مربّع حوار لاختيار وجهة الحفظ في كلّ مرّة يُنفَّذ فيها، فلا يصلح للمعالجة الدفعيّة دون إشراف.12
- تعلن Microsoft رسميّاً عدم دعم وعدم التوصية بأتمتة COM لِـ Office من الخوادم أو الخدمات. فـ Office مصمَّم على افتراض سطح مكتب تفاعليّ وملفّ تعريف مستخدم، وفي البيئات غير التفاعليّة ودون إشراف قد يسبِّب سلوكاً غير مستقرّ أو توقّفاً تامّاً (deadlock).13
- تعمل خدمات Windows ضمن الجلسة رقم 0، لذا لا يمكنها عرض مربّعات حوار، ولا يمكنها الاعتماد على الطابعة الافتراضيّة المرتبطة بجلسة المستخدم. عند تصميم معالجة مقيمة تتضمّن الطباعة، ضع هذا الحدّ في اعتبارك منذ البداية.14
- نسيان تحرير كائنات GDI (المقابض/handles) أثناء التشغيل طويل الأمد يُفضي إلى بلوغ الحدّ الأقصى لكلّ عمليّة وحدوث انهيار. هذا الحدّ قيمة منتهية قابلة للضبط عبر
GDIProcessHandleQuotaفي Registry، وليس ممكناً حجزه إلى ما لا نهاية.15
جدول القرار ── المتطلّب × الوسيلة
| المتطلّب | الوسيلة المناسبة | السبب |
|---|---|---|
| طباعة قائمة بسيطة أو بضع فواتير (WinForms) | PrintDocument |
يكتمل بالفئات القياسيّة وحدها، ويمكن إعادة استخدام أصول رسم GDI+ القائمة مباشرةً |
| تقرير كثير الخطوط الفاصلة، تخطيط معقّد أو مقاسات ورق متعدّدة | مكتبة تقارير، أو تخطيط ثابت عبر FixedDocument |
تكلفة كتابة حساب الإحداثيّات والتحكّم بفواصل الصفحات وتنضيد النصّ اليابانيّ ذاتيّاً كبيرة |
| طباعة دفعيّة ضخمة (تشغيل ليليّ دون إشراف) | استدعاء PrintDocument.Print() برمجيّاً / الإرسال المباشر إلى XpsDocumentWriter |
يلزم الإكمال دون إظهار أيّ مربّع حوار، مع تجنّب الاعتماد على الطابعة الافتراضيّة |
| يكفي حفظ PDF فقط (دون استخدام الطباعة) | إنشاء PDF مباشرةً عبر مكتبة توليد PDF | لا يعتمد على مربّع حوار الطباعة أو الطابعة الافتراضيّة أو حالة المُجزِّئ (spooler) |
| المعاينة إلزاميّة، وسير عمل المراجعة الداخليّ مهمّ | PrintPreviewDialog (في WinForms)، DocumentViewer (في WPF) |
يمكن استخدام واجهة المستخدم القياسيّة للمراجعة قبل الطباعة مباشرةً |
| إعادة استخدام قالب Excel قائم | التوليد المباشر عبر Open XML SDK أو ما شابه، أو أتمتة COM محدودة | يمكن إعادة استخدام الأصول، لكن تجنَّب أتمتة COM في خدمة مقيمة |
فيما يلي، نستعرض تفاصيل كلّ بند.
2. الحدّ الأدنى من فهم آليّة الطباعة في Windows
تتكوّن بنية الطباعة في Windows من مُجزِّئ الطباعة (print spooler) وبرنامج تشغيل خاصّ بكلّ طابعة. ويكفي أن يستدعي التطبيق دوالّاً غير مرتبطة بجهاز معيّن ليرسل مهامّ الطباعة إلى وجهات متنوّعة كالطابعات الليزريّة، وراسمات المتّجهات (vector plotters)، والطابعات النقطيّة (raster)، والفاكس.16
ينقسم مسار الطباعة إلى نظامين رئيسيَّين.
- مسار طباعة GDI. عند طباعة تطبيق GDI من نوع Win32، يقوم محرّك رسوميّات GDI إمّا بتخزين أوامر الرسم كملفّ EMF (تعريف موسّع للملفّات الوصفيّة) في المُجزِّئ، أو برسم صورة قابلة للطباعة مباشرةً بالتعاون مع برنامج تشغيل الطابعة وإرسالها إلى المُجزِّئ. تطبيقات WinForms التقليديّة تمرّ عبر هذا المسار.166
- مسار طباعة XPS. مسار مخصَّص لبرامج تشغيل الطابعات المبنيّة على XPS (XML Paper Specification)، وتطبيقات WPF تستخدم هذا المسار أصلاً، فترسل إلى المُجزِّئ بصيغة مستند XPS. وإن لم تكن الطابعة المستهدَفة تستخدم برنامج تشغيل XPSDrv، يُحوَّل الشكل تلقائيّاً إلى صيغة GDI قبل الإرسال.67
لماذا يختلف الشكل على الشاشة عن نتيجة الطباعة؟ السبب الأشيع هو الخلط بين قيم DPI. فنظام إحداثيّات Graphics الخاصّ بالشاشة يُبنى عادةً حول 96 DPI تقريباً، بينما يعمل Graphics الخاصّ بالطابعة، الذي يحدِّده PageSettings.PrinterResolution، بدقّة أعلى بكثير تبلغ غالباً نحو 600 DPI.3 وتمرير الإحداثيّات بالبكسل المحسوبة للشاشة كما هي إلى Graphics الخاصّ بالطباعة يجعل الرسم أصغر بكثير ممّا هو مقصود (أو أكبر حسب إعداد الدقّة العالية). في الطباعة، الأسلم دوماً هو بناء الإحداثيّات استناداً إلى «أجزاء المئة من البوصة» (1/100 بوصة) أو الحجم الفعليّ (ملّيمتر/بوصة)، بتصميم لا يعتمد على دقّة Graphics.
وممّا يُغفَل عنه أيضاً هو المنطقة الفيزيائيّة غير القابلة للطباعة التي تملكها الطابعة. فمعظم الطابعات لا تستطيع الطباعة على نطاق بضعة ملّيمترات من حافّة الورقة. يمكن الحصول على هذا القيد الفيزيائيّ عبر PageSettings.HardMarginX/HardMarginY، وتشرحه الوثائق الرسميّة بأنّه «يمثّل الهامش الفيزيائيّ الذي تحدِّده الطابعة».4 أمّا الهامش المنطقيّ الذي يحدِّده التطبيق فهو PageSettings.Margins (القيمة الافتراضيّة بوصة واحدة من كلّ جهة)، لكن حتّى مع ضبطه على صفر، لا يمكن الإخراج إلى ما هو أقرب من الهامش الفيزيائيّ للطابعة (Hard Margin).5 ومعظم الأعطال من نوع «ضبطتُ الهامش على صفر ومع ذلك يُقصّ الطرف/الانزياح أكبر ممّا توقّعت» سببها عدم إدراك وجود هذا الهامش الفيزيائيّ.
3. WinForms: الممارسة العمليّة لِـ System.Drawing.Printing
تُبنى الطباعة في WinForms حول مكوّن PrintDocument. التدفّق الأساسيّ هو: إنشاء PrintDocument، ثمّ ضبط PrinterSettings أو DefaultPageSettings، ثمّ تنفيذ الرسم الفعليّ في حدث PrintPage، ثمّ بدء الطباعة عبر التابع Print().1
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Printing;
using System.Windows.Forms;
public sealed class ReportPrinter
{
// بيانات موضوع الطباعة. يُفترَض أنّ كلّ سطر يمثّل بند فاتورة واحد
private readonly IReadOnlyList<string> _lines;
private int _lineIndex;
public ReportPrinter(IReadOnlyList<string> lines) => _lines = lines;
public void Print(string printerName)
{
using var document = new PrintDocument();
document.DocumentName = "تفاصيل الطلب";
if (!string.IsNullOrEmpty(printerName))
{
document.PrinterSettings.PrinterName = printerName;
}
// تصبح القيمة هنا false عند تحديد اسم طابعة غير معروف (لا يصلح للكشف عن حالة عدم الاتّصال)
if (!document.PrinterSettings.IsValid)
{
throw new InvalidOperationException(
$"الطابعة المحدَّدة غير موجودة: {printerName}");
}
_lineIndex = 0;
document.PrintPage += Document_PrintPage;
document.Print();
}
private void Document_PrintPage(object? sender, PrintPageEventArgs e)
{
// MarginBounds هي المنطقة القابلة للرسم التي تعكس الهامش المنطقيّ (Margins).
// PageBounds هي الورقة كلّها، وحتّى الرسم داخل الهامش الفيزيائيّ (Hard Margin) قد يُقصّ
using var font = new Font("メイリオ", 10);
float lineHeight = font.GetHeight(e.Graphics);
float y = e.MarginBounds.Top;
while (_lineIndex < _lines.Count)
{
if (y + lineHeight > e.MarginBounds.Bottom)
{
// انتهت هذه الصفحة هنا. توجد أسطر متبقّية، فانتقل إلى الصفحة التالية
e.HasMorePages = true;
return;
}
e.Graphics.DrawString(_lines[_lineIndex], font, Brushes.Black,
e.MarginBounds.Left, y);
y += lineHeight;
_lineIndex++;
}
// اكتمل رسم جميع الأسطر، فهذه هي الصفحة الأخيرة
e.HasMorePages = false;
}
}
النمط النموذجيّ لعطل «الصفحة الثانية لا تظهر» هو نسيان ضبط HasMorePages صراحةً على false (القيمة الافتراضيّة هي false)، أو على العكس، خطأ في كتابة التفريع الشرطيّ يجعلها تخرج دائماً بقيمة false. علماً بأنّ حدث PrintPage يُستدعى تكراراً حتّى تصبح هذه الخاصيّة false، فاحرص على تحديد «هل ما تزال هناك أسطر يجب رسمها» صراحةً قبل ضبطها.12 وإذا أردتَ تطبيق إعدادات مختلفة لكلّ صفحة (كحجم الورق أو اتّجاهه)، يمكن استغلال حدث QueryPageSettings أيضاً بالإضافة إلى PrintPage.
النمط النموذجيّ لعطل «انزياح الهوامش» هو بناء الإحداثيّات استناداً إلى e.Graphics.VisibleClipBounds أو e.PageBounds، مع تجاهل MarginBounds (المنطقة التي تعكس الهامش المنطقيّ) وَHardMarginX/HardMarginY (القيد الفيزيائيّ للطابعة).4 وخصوصاً أنّ طابعة جهاز التطوير قد يكون هامشها الفيزيائيّ صغيراً فلا تظهر مشكلة بالصدفة، بينما يكون هامش طابعة أخرى لدى العميل أكبر فتبدو الخطوط الفاصلة أو العناصر ناقصة — وهذه حادثة تحدث بسهولة. التحقّق من الطباعة على أجهزة متعدّدة خطوة كثيراً ما تُغفَل أثناء التطوير.
إذا أردتَ توفير معاينة، استخدم PrintPreviewDialog، ويكفي تعيين نفس PrintDocument إلى خاصيّة Document لإعادة استخدام منطق حدث PrintPage كما هي.1 وإذا أردتَ تبديل الإخراج حسب دقّة الطابعة، احصل على الخيارات من PrinterSettings.PrinterResolutions واضبطها في PageSettings.PrinterResolution.3
4. WPF: FlowDocument / FixedDocument وPrintDialog
في WPF، يختلف اختيار المحور بين FlowDocument وَFixedDocument باختلاف طبيعة المستند.
FlowDocument. مستند يعطي أولويّة لقابليّة القراءة ويُعيد تخطيط المحتوى ديناميكيّاً حسب متغيّرات وقت التشغيل كحجم النافذة وإعدادات الخطّ. يمتلك افتراضيّاً ميزات كالبحث والترقيم عبر الصفحات وتغيير وضع العرض.8FixedDocument. مستند من نوع WYSIWYG يعطي أولويّة للدقّة، يتحكّم فيه التطبيق تحكّماً كاملاً بالتخطيط. مناسب عندما تحتاج إلى إعادة إنتاج بدقّة عالية على جهاز العرض والطباعة.9
الخطّ الأساسيّ للحسم هو: FlowDocument للتقارير الداخليّة التي تعطي أولويّة لسهولة القراءة بأسلوب التدفّق، وFixedDocument للفواتير والملصقات التي يُراد فيها تثبيت مواضع الخطوط الفاصلة والعناصر بدقّة البكسل.
تُنجَز الطباعة الأساسيّة عبر System.Windows.Controls.PrintDialog. وهذه الفئة مختلفة عن System.Windows.Forms.PrintDialog، وهي خاصّة بـ WPF فقط.10
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
public static class FlowDocumentPrinter
{
public static void Print(FlowDocument document, string jobName)
{
var printDialog = new PrintDialog();
// اطبع فقط إذا ضغط المستخدم على موافق
if (printDialog.ShowDialog() != true)
{
return;
}
// لأنّ افتراض حجم الصفحة في FlowDocument يختلف بين العرض والطباعة،
// اضبط حجم الصفحة والهامش الخاصَّين بموضوع الطباعة على المنطقة القابلة للطباعة لدى الطابعة
var paginator = ((IDocumentPaginatorSource)document).DocumentPaginator;
paginator.PageSize = new Size(
printDialog.PrintableAreaWidth, printDialog.PrintableAreaHeight);
printDialog.PrintDocument(paginator, jobName);
}
}
إذا أردتَ تحديداً دقيقاً لحجم الورق وصينيّة التغذية والطباعة على الوجهَين وما شابه، استخدم PrintQueue وَPrintTicket/PrintCapabilities. يمثّل PrintTicket تعليمات مهمّة الطباعة، ويمثّل PrintCapabilities الميزات التي تدعمها الطابعة فعليّاً، والأسلوب المتعارف عليه هو دمج الإعدادات المحدَّدة والتحقّق منها إلى قيم صالحة خاصّة بالطابعة عبر PrintQueue.MergeAndValidatePrintTicket قبل استخدامها.711
using System.Linq;
using System.Printing;
public static class DuplexPrintTicketBuilder
{
public static PrintTicket BuildDuplexTicket(PrintQueue printQueue)
{
PrintCapabilities capabilities = printQueue.GetPrintCapabilities();
// تحقّق مسبقاً ممّا إذا كانت الطابعة تدعم الطباعة على الوجهَين (تجليد الحافّة الطويلة)
bool supportsDuplex = capabilities.DuplexingCapability
.Contains(Duplexing.TwoSidedLongEdge);
var requestedTicket = new PrintTicket();
if (supportsDuplex)
{
requestedTicket.Duplexing = Duplexing.TwoSidedLongEdge;
}
// ادمج طلب الطباعة على الوجهَين فقط مع PrintTicket الافتراضيّ للمستخدم.
// تُستبدَل العناصر غير المدعومة تلقائيّاً بقيم صالحة
ValidationResult result = printQueue.MergeAndValidatePrintTicket(
printQueue.UserPrintTicket, requestedTicket);
return result.ValidatedPrintTicket;
}
}
إذا بنيتَ تقريراً بتخطيط ثابت عبر FixedDocument وأردت إرساله مباشرةً إلى مسار طباعة XPS، أضِف المهمّة إلى PrintQueue عبر تابعَي Write/WriteAsync الخاصّين بـ XpsDocumentWriter. وإن كانت الطابعة المستهدَفة لا تدعم XPSDrv، يُحوَّل الشكل تلقائيّاً إلى صيغة GDI داخليّاً قبل الإرسال، فلا حاجة لجهة الاستدعاء لأن تعي المسار المستخدَم.7
5. خيارات إخراج PDF
متطلّب «أريد الإخراج بصيغة PDF» يختلف تصميمه فعليّاً باختلاف أيّ من الحالات الثلاث التالية يُقصَد به.
١. يكفي أن يتمكّن المستخدم من اختيار حفظ PDF يدويّاً كامتداد للطباعة
٢. يُراد توليد PDF من داخل التطبيق عبر مربّع حوار الطباعة
٣. لا طباعة على الإطلاق، بل يُراد توليد عدد كبير من ملفّات PDF فقط عبر معالجة دفعيّة دون إشراف
Microsoft Print to PDF ميزة موجَّهة للحالتَين 1 و2. وهي ميزة مضمَّنة افتراضيّاً منذ Windows 10 فما بعده، بموقع طابور طباعة وحزمة برنامج تشغيل، وتظهر من التطبيق الذي يطبع كإحدى الطابعات العاديّة.12 لكنّ جوهرها آليّة مبنيّة على افتراض السلوك الحواريّ «سؤال عن اسم ملفّ الوجهة في كلّ مرّة تُطبَع فيها»، وحتّى رسميّاً لا تُوفَّر وسيلة تحكّم إلّا على مستوى تمكين المدير من إزالة طابور الطباعة وحزمة برنامج التشغيل هذه من الصورة (image).12 وهذا غير مناسب من الناحية التصميميّة لاستخدام الحالة 3، أي إخراج PDF بصمت مع مسار ملفّ ثابت في معالجة دفعيّة كبيرة دون إشراف. إذا أردتَ تحقيق الحالة 3، فالأسلوب المباشر هو استخدام مكتبة تولِّد PDF مباشرةً دون المرور عبر خطّ أنابيب الطباعة.
محاور المقارنة عند اختيار مكتبة توليد PDF — قبل الترويج لمنتج معيّن، فإنّ تنظيم متطلّبات شركتك وفق المحاور التالية يمنع تذبذب الاختيار.
| المحور | ما يجب التحقّق منه |
|---|---|
| الترخيص | هل هي مفتوحة المصدر (من نوع MIT/Apache أم من نوع GPL/AGPL) أم ترخيص تجاريّ؟ عند توزيع تطبيقك أو بيعه خارج الشركة، تحقّق دوماً من المصدر الأوّليّ (نصّ الترخيص أو الموقع الرسميّ لمقدّم المكتبة) للتأكّد من أنّ شروط ترخيص المكتبة (إلزاميّة الكشف عن الشيفرة المصدريّة، شروط إعادة التوزيع، جواز الاستخدام التجاريّ) لا تتعارض مع شكل التوزيع لديك |
| التعامل مع الخطوط اليابانيّة | هل تدعم تضمين مجموعة فرعيّة (subset) من الخطوط اليابانيّة؟ هل يمكن تضمين الخطّ فعليّاً داخل PDF دون تضمين (لتجنّب ظهور رموز غير مقروءة في بيئة لا تملك الخطّ)؟ وإن كان العمل يحتاج الكتابة العموديّة أو الأشكال المتغايرة للحروف (IVS)، فحالة دعمها |
| ميزات موجَّهة للتقارير | هل تدعم تعريف التقارير القائم على القوالب؟ توليد الباركود ورموز الاستجابة السريعة (QR)؟ التوقيع الإلكترونيّ؟ دعم PDF/A (مواصفة الحفظ طويل الأمد)؟ سهولة نقل تخطيط التقارير الورقيّة القائمة كما هي؟ |
| التبعيّات وبيئة التشغيل | هل تعمل في بيئة خادم (Windows Server دون واجهة رسوميّة، أو حاوية)؟ هل توجد تبعيّة على مكتبات أصليّة (native)؟ ما مدى نضج العمل في بيئة .NET Core/.NET (غير Framework)؟ |
| الأداء | استهلاك الذاكرة عند توليد دفعات كبيرة من الصفحات والمهامّ، وسلوكها عند التوليد المتوازي |
تختلف شروط الترخيص باختلاف المنتج والإصدار، لذا تحقّق دائماً من الوثائق الرسميّة لمقدّم المكتبة أو من نصّ الترخيص نفسه قبل اتّخاذ قرار الاعتماد. لا يوصي هذا المقال باسم مكتبة أو منتج معيّن، بل يعرض «المحاور» الخاصّة بالمقارنة فقط.
6. خيار التقارير عبر Excel / Word
إذا كان هناك تقرير يُشغَّل بالفعل عبر قالب Excel، فقد يكون بناء بنية تستفيد من الأصول القائمة أكثر واقعيّةً من إعادة بنائه من الصفر. تنقسم الطريقة إلى نوعين رئيسيَّين.
- توليد وتحرير xlsx/docx مباشرةً عبر Open XML SDK أو ما شابه. يعمل بثبات حتّى في بيئة خادم لأنّه يقرأ ويكتب صيغة الملفّ (ZIP + XML) مباشرةً دون تشغيل الملفّ التنفيذيّ لِـ Excel/Word. يتوافق جيّداً مع بنية تستفيد من الخطوط الفاصلة وتنسيقات القالب كما هي وتُدرِج القيم فقط، وتفاصيله مرتَّبة في «كيف تبني إخراج تقارير Excel - COM/Open XML/القوالب».
- تشغيل Excel.Application فعليّاً عبر Excel COM Interop. يُختار عندما يُراد إعادة استخدام مصنّف Excel قائم يتضمّن وحدات ماكرو أو إعادة حسابات معقّدة كما هو، لكنّه عرضة لمشكلة بقاء عمليّة EXCEL.EXE بسبب عدم تحرير مراجع COM، وقد عالجنا الإجراءات الوقائيّة ومعايير الحسم في «مشكلة بقاء EXCEL.EXE عند التعامل مع Excel من C#».
المهمّ هنا هو أنّ Microsoft تعلن رسميّاً «عدم التوصية وعدم الدعم» لأتمتة COM لِـ Office من جانب الخادم أو من خدمة Windows. فتطبيقات Office مصمَّمة على افتراض سطح مكتب تفاعليّ وملفّ تعريف مستخدم مسجَّل الدخول، وذُكِر أنّ تشغيلها في بيئة دون إشراف وغير تفاعليّة قد يسبِّب سلوكاً غير مستقرّ أو توقّفاً تامّاً (deadlock).13 وحتّى من الناحية الترخيصيّة، فإنّ استخدام أتمتة من جانب الخادم لتقديم وظائف Office إلى العميل غير متوقَّع في اتّفاقيّة الترخيص العاديّة (EULA).13
لذلك، إذا أردت دمج إخراج التقارير في خدمة مقيمة أو مهمّة دفعيّة، فالأسلم هو تجنّب بنية تشغِّل Excel.exe فعليّاً وقت التنفيذ (COM Interop) حتّى لو أعدتَ استخدام قالب Excel قائم، والميل إلى التوليد المباشر للملفّات عبر Open XML SDK أو ما شابه. وإن كانت COM Interop ضروريّة حتماً، فالفصل الواقعيّ هو تكوينها كتطبيق مقيم (وليس خدمة) يعمل ضمن جلسة مستخدم مسجَّل الدخول تفاعليّاً.
7. مطبّات العمل الفعليّ
الطباعة من خدمات Windows والجلسة رقم 0
ليست قليلة الاستشارات التي تطلب دمج معالجة الطباعة في خدمة مقيمة، لكنّ خدمات Windows تعمل ضمن الجلسة رقم 0. والجلسة رقم 0، منذ Windows Vista فما بعده، جلسة محجوزة حصراً للعمليّات الخاصّة بالخدمات وغير المرتبطة بمستخدم تفاعليّ، ومنفصلة عن الجلسة التفاعليّة للمستخدم.14 وبسبب هذا الفصل، حتّى لو عرضت الخدمة مربّع حوار فلن يراه المستخدم، وكتابة شيفرة تنتظر إدخال المستخدم (كانتظار عرض مربّع حوار الطباعة) تبدو وكأنّها متجمّدة (hang).14
ومن المطبّات الشائعة أيضاً في العمل الفعليّ هي طريقة ظهور الطابعات الشبكيّة أو الطابعات المرتبطة (mapped) بمستخدم بعينه. فالطابعة الشبكيّة التي وصلها المستخدم بعد تسجيل الدخول غالباً ما تظهر مرتبطة بالجلسة التفاعليّة لذلك المستخدم، ولا تظهر بالطريقة نفسها من جلسة أخرى (سياق عمليّة خدمة تعمل ضمن الجلسة رقم 0). المعالجة المقيمة التي تتضمّن طباعة يجب أن تتحقّق منذ بداية التصميم من «أيّ طابعة تظهر من أيّ حساب وأيّ جلسة». وقد رتّبنا الصورة العامّة لفصل الجلسات في «كيف نفهم فصل الجلسات في Windows»، وأساسيّات التنفيذ والتشغيل كخدمة في «كيفيّة بناء خدمات Windows وتشغيلها».
السلوك عند غياب الطابعة أو كونها غير متّصلة
تحديد اسم طابعة غير موجودة في PrinterSettings.PrinterName، أو التنفيذ في بيئة لم تُضبَط فيها طابعة افتراضيّة، يسبِّب استثناءً أو فشلاً عند محاولة الطباعة. النقطتان الأدنى اللتان ينبغي مراعاتهما هما: التحقّق مسبقاً عبر PrinterSettings.IsValid، وتصميم إعادة محاولة وإخطار بالخطأ يفترض إمكانيّة فشل الطباعة. وإذا كانت الطابعة الشبكيّة غير متّصلة مؤقّتاً، فقد تُضاف المهمّة إلى المُجزِّئ لكنّ الإخراج الفعليّ يبقى متوقّفاً، لذا لا تكتفِ باعتبار نتيجة الطباعة «تمّ الإرسال»، بل ادرس أيضاً آليّة للتحقّق من حالة المهمّة عند الحاجة.
خطر الاعتماد على الطابعة الافتراضيّة
التنفيذ الذي لا يحدِّد PrinterSettings.PrinterName صراحةً ويعتمد دوماً على «الطابعة الافتراضيّة» يصبح مخاطرة كلّما طال أمد التشغيل. لأنّ تغيّرات البيئة — كتغيير المستخدم للطابعة الافتراضيّة، أو نقل حاسوب محمول إلى مكان آخر فتتغيّر الطابعة الافتراضيّة معه — قد ترسل المخرجات إلى وجهة غير مقصودة. بالنسبة للتقارير المهمّة للعمل، ينبغي الاحتفاظ باسم الطابعة المستهدَفة صراحةً في ملفّ إعدادات أو شاشة إعدادات داخل التطبيق، بتصميم لا ينجرّ وراء تغيّرات الطابعة الافتراضيّة.
تسريب مقابض GDI أثناء التشغيل طويل الأمد
إذا كتبتَ في معالجة الطباعة شيفرة لا تحرِّر موارد GDI/GDI+ مثل Graphics وَFont وَBrush وَPen بشكل مؤكَّد عبر using أو Dispose()، فستنفد مقابض كائنات GDI بعد تشغيل طويل الأمد، وتبدأ استدعاءات API المرتبطة بالرسم بالفشل. ولمقابض كائنات GDI حدّ أقصى افتراضيّ لكلّ عمليّة، وهو مورد منتهٍ قابل للضبط ضمن نطاق 256 إلى 65,536 عبر GDIProcessHandleQuota في Registry.15 وكلّما كان التطبيق المقيم يطبع بكثرة، كان هذا النوع من التسريب أكثر ميلاً إلى أن يصبح عطلاً لا يظهر إلّا في بيئة الإنتاج الفعليّة. وقد شرحنا كيفيّة البحث عن تسريب المقابض وتحديده بشكل ملموس في «تحقيق انهيار كاميرا صناعيّة بعد تشغيل طويل الأمد - قسم تسريب المقابض» الذي يتناول حالة انهيار كاميرا صناعيّة بعد تشغيل طويل الأمد.
مقالات ذات صلة
- كيف تبني إخراج تقارير Excel - COM/Open XML/القوالب
- مشكلة بقاء EXCEL.EXE عند التعامل مع Excel من C# ── نمط تحرير مراجع COM وقرار الاستبدال
- كيفيّة بناء خدمات Windows وتشغيلها ── من المفاضلة مع جدولة المهامّ إلى تحويل BackgroundService إلى خدمة
- كيف نفهم فصل الجلسات في Windows ── الجلسة 0، RDP، والتشغيل المتزامن لعدّة مستخدمين
- تحقيق انهيار كاميرا صناعيّة بعد تشغيل طويل الأمد - قسم تسريب المقابض
مجالات الاستشارة ذات الصلة
تتعامل شركة كومورا سوفت ذ.م.م. مع تصميم ميزات الطباعة وإخراج التقارير في تطبيقات أعمال Windows، وإعادة بناء التقارير بالاستفادة من أصول Excel القائمة، والاستشارات التقنيّة حول تصميم الطباعة والإخراج من الخدمات المقيمة.
المراجع
-
Microsoft Learn, PrintDocument Class. حول دور
PrintDocument(ضبطDocumentNameوَPrinterSettingsوبدء الطباعة عبرPrint())، والإرشاد إلى ضرورة استخدام مساحة الاسمSystem.Printingعند الطباعة من WPF. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, PrintPageEventArgs.HasMorePages Property. حول كون القيمة الافتراضيّة
false، وأنّ حدثPrintPageيتكرّر حدوثه حتّى تصبح هذه الخاصيّةfalse. ↩ ↩2 -
Microsoft Learn, PageSettings.PrinterResolution Property. حول كون القيمة الافتراضيّة لدقّة طباعة الصفحة هي الدقّة الافتراضيّة للطابعة، وإمكانيّة الحصول على قائمة الدقّات المتاحة للاختيار من
PrinterSettings.PrinterResolutions. ↩ ↩2 ↩3 -
Microsoft Learn, PageSettings.HardMarginX Property. حول كون الهامش الفيزيائيّ (Hard Margin) يمثّل الهامش الفيزيائيّ الذي تحدِّده الطابعة. ↩ ↩2 ↩3
-
Microsoft Learn, PageSettings.Margins Property. حول كون القيمة الافتراضيّة لهامش الصفحة بوصة واحدة من كلّ جهة، وإمكانيّة حساب المنطقة القابلة للطباعة بالجمع بين ذلك وخاصيّة
Boundsفي حدثPrintPage. ↩ ↩2 -
Microsoft Learn, Windows Print Path Overview. حول وجود مسارَي طباعة رئيسيَّين في Windows: مسار طباعة GDI (من تطبيقات Win32) ومسار طباعة XPS (من تطبيقات WPF أو XPS Print API). ↩ ↩2 ↩3
-
Microsoft Learn, Printing documents overview. حول استخدام تطبيقات WPF مسار طباعة XPS، والطباعة الأساسيّة عبر
PrintDialog، والطباعة المتقدّمة عبرPrintTicket/PrintCapabilities/PrintQueue/XpsDocumentWriter، والتحويل التلقائيّ إلى GDI للطابعات غير الداعمة لِـ XPSDrv. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Flow Document Overview. حول كون
FlowDocumentمستنداً يعطي أولويّة لقابليّة القراءة ويُعيد تخطيط المحتوى حسب متغيّرات وقت التشغيل كحجم النافذة والدقّة. ↩ ↩2 -
Microsoft Learn, FixedDocument Class. حول كون
FixedDocumentتصميم WYSIWYG يعطي أولويّة لإعادة الإنتاج الدقيقة على جهاز العرض والطباعة، وأنّ فلسفة تصميمه تختلف عنFlowDocument. ↩ ↩2 -
Microsoft Learn, PrintDialog Class. حول كون
System.Windows.Controls.PrintDialogمربّع حوار يبنيPrintTicketوَPrintQueueحسب إدخال المستخدم، وأنّه فئة مختلفة عنSystem.Windows.Forms.PrintDialog. ↩ ↩2 -
Microsoft Learn, How to: Validate and Merge PrintTickets. حول إجراء التحقّق من الميزات المدعومة للطابعة عبر
PrintQueue.GetPrintCapabilities، ودمج المحتوى المطلوب والتحقّق منه إلىPrintTicketصالح خاصّ بالطابعة عبرMergeAndValidatePrintTicket. ↩ ↩2 -
Microsoft Learn, RemoveMPDW. حول كون Microsoft Print to PDF ميزة اختياريّة مثبَّتة افتراضيّاً، تُهيَّأ وتُحذَف على مستوى طابور الطباعة وحزمة برنامج التشغيل. ↩ ↩2 ↩3
-
Microsoft Support, Considerations for server-side Automation of Office. حول عدم توصية ودعم Microsoft لأتمتة Office من تطبيقات عميل غير مأهولة وغير تفاعليّة، بما فيها ASP/ASP.NET/DCOM/خدمات NT، وأنّ Office مصمَّم على افتراض سطح مكتب تفاعليّ وملفّ تعريف مستخدم. ↩ ↩2 ↩3
-
Microsoft Learn, Service Changes for Windows Vista - Session 0 Isolation. حول حجز الجلسة رقم 0 منذ Windows Vista فما بعده حصراً للتطبيقات الخاصّة بالخدمات وغير المرتبطة بمستخدم تفاعليّ، وعدم قدرة الخدمات على عرض مربّعات حوار مباشرةً بعد ذلك. ↩ ↩2 ↩3
-
Microsoft Learn, GDI Objects. حول امتلاك مقابض كائنات GDI حدّاً أقصى افتراضيّاً لكلّ عمليّة، وإمكانيّة ضبط هذا الحدّ ضمن نطاق 256 إلى 65,536 عبر قيمة Registry المسمّاة
GDIProcessHandleQuota. ↩ ↩2 -
Microsoft Learn, Introduction to printing. حول تكوّن بنية الطباعة في Windows من المُجزِّئ وبرنامج تشغيل الطابعة، وآليّة تخزين أوامر رسم تطبيقات Win32 GDI كملفّات EMF في المُجزِّئ. ↩ ↩2
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
أيقونات علبة النظام والإشعارات المنبثقة في تطبيقات Windows — مزالق NotifyIcon وكيفية اختيار AppNotification المناسب
دليل عملي لإبقاء تطبيق Windows الخاص بالأعمال مقيمًا في علبة النظام (منطقة الإشعارات) وإخطار المستخدم عبر الإشعارات المنبثقة (Toast). يتن...
تعدد اللغات في تطبيقات WinForms/WPF ── الممارسة العملية لملفات resx وتجميعات الأقمار الصناعية (Satellite Assemblies) وتبديل الثقافة (Culture)
نستعرض من منظور عملي تعدد اللغات في تطبيقات سطح المكتب على Windows، بما في ذلك الفرق بين CurrentCulture وCurrentUICulture، وآلية الموارد ...
التعهيد الخارجي والتطوير التعاقدي لتطبيقات Windows: ما ينبغي تنظيمه قبل الطلب
قبل تكليف جهة خارجية بتطوير تطبيق Windows تعاقديّاً، إليك النقاط التي ينبغي تنظيمها: تعديل البرمجيّات القائمة، وتكامل الأجهزة، وCOM/Activ...
CI/CD عمليّ لتطبيقات WinForms / WPF ── أتمتة من البناء إلى التوقيع والتوزيع عبر GitHub Actions
دليل عمليّ لبناء CI/CD لتطبيقات WinForms / WPF عبر GitHub Actions. يشمل YAML الأدنى للبناء + الاختبار على windows-latest، وترقيم الإصدار ...
لا تُحِط HttpClient بـ using ── الممارسة العمليّة للاتّصال عبر HTTP في تطبيقات C# للأعمال (أنماط الإنشاء، وضبط المهلة، وإعادة المحاولة)
إنشاء HttpClient داخل using في كلّ مرّة يؤدّي إلى استنزاف المقابس (sockets)، وجعله static يجعله لا يتابع تغيّر DNS ── نشرح آليّة هاتين ال...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
لأنّ تصميم وتنفيذ ميزات الطباعة وإخراج PDF في تطبيقات الأعمال يدخل ضمن نطاق استشارات تطوير تطبيقات Windows.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- أيّهما ينبغي استخدامه للطباعة: PrintDocument أم WPF؟
- إن كانت الشاشة WinForms فاستخدم PrintDocument بشكل مباشر، وإن كانت WPF فاستخدم FlowDocument/FixedDocument. لا حاجة لخلط التقنيّتين، والمعيار العمليّ للحسم هو مطابقة إطار عمل الشاشة الموجود في التطبيق القائم. وإن اخترت WPF في تطوير جديد، فتحديد ما إذا كان محور العمل هو FlowDocument أم FixedDocument بناءً على كون التقرير قائماً على التدفّق أم على تخطيط ثابت، مسبقاً، يمنع التردّد لاحقاً.
- هل ينبغي شراء مكتبة تقارير؟
- إذا كانت تكلفة بناء تقارير معقّدة كثيرة الخطوط الفاصلة، ودعم مقاسات ورق متعدّدة، وتضمين خطوط يابانيّة أو الكتابة العموديّة، ذاتيّاً عبر رسم GDI+/WPF كبيرةً، فغالباً ما تكون تكلفة إدخال مكتبة تقارير جاهزة أقلّ. وعلى العكس، غالباً ما يكفي التنفيذ الذاتيّ عبر PrintDocument أو FixedDocument لتقرير بسيط بحجم قائمة A4، لذا فالأسلم هو تقدير مدى تعقيد المتطلّبات أوّلاً ثمّ الحسم.
- هل يجوز بناء تصميم يُنشئ PDF فقط دون استخدام الطباعة؟
- نعم، وهذا وارد تماماً. فـ PDF ليس إلّا صيغة إخراج واحدة، والتوليد المباشر عبر مكتبة PDF لا يعتمد على مربّع حوار الطباعة أو الطابعة الافتراضيّة، وغالباً ما يتوافق بشكل أفضل مع المعالجة الدفعيّة والأتمتة. أمّا Microsoft Print to PDF فهو برنامج تشغيل (driver) مبنيّ على افتراض التفاعل الحواريّ، ولا يصلح لتوليد PDF دون إشراف بشريّ.
- هل يمكن دعم طابعات الملصقات (labels) أو طابعات الإيصالات (receipts)؟
- ممكن، لكن قد يصعب أحياناً تحقيق ذلك كامتداد لِـ PrintDocument/FixedDocument العامّة. فمعظم طابعات الملصقات لها لغة أوامر خاصّة بها أو SDK خاصّ، وكثيراً ما تُتحكَّم بها عبر مسار منفصل عن مسار الطباعة القياسيّ في Windows. ابدأ أوّلاً بالتحقّق ممّا إذا كان الجهاز المستهدَف يتصرّف كبرنامج تشغيل طابعة عاديّ في Windows، أو أنّه يحتاج إلى تحكّم عبر SDK مخصّص.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.
روابط عامة