التعامل مع الدقّة العالية (DPI) في WPF ── أسباب الضبابيّة والتلطّخ رغم أنّه «يُفترَض أن يكون محصّناً ضدّ DPI» وطرق العلاج

· آخر تحديث: · · WPF, DPI العالي, Windows, .NET, C#, .NET Framework, XAML, UI, الاستشارات التقنية

«أليس صحيحاً أنّه لا حاجة لفعل أيّ شيء بخصوص دعم DPI إن كان التطبيق مبنيّاً بـ WPF؟» سؤال نتلقّاه كثيراً في استشارات الدقّة العالية (high DPI). نصف الإجابة صحيح، ونصفها غير صحيح. والأعراض التي تُطرَح فعليّاً هي: «تبدو الشاشة واضحة على الحاسوب المحمول وحده، لكن عند توصيله بشاشة خارجيّة في قاعة الاجتماعات تتلطّخ الشاشة كلّها»، أو «النصوص واضحة لكنّ أيقونات شريط الأدوات وحدها ضبابيّة»، أو «تختلف سماكة خطوط الحدود في القائمة من مكان لآخر أو تختفي»، أو «الجزء المُدمَج من عنصر تحكّم تقارير قديم داخل الشاشة صغير وحده». وكلّها أعراض تحدث فعليّاً في تطبيقات WPF التي يُفترَض أنّها «محصّنة ضدّ DPI».

في المقال السابق «التعامل مع الدقّة العالية في WinForms»، نظّمنا آليّة تحجيم DPI في Windows، وأوضاع الوعي بـ DPI (‏Unaware / System Aware / Per-Monitor V2)، وكيفيّة إنقاذ تطبيقات WinForms من عصر كتابة 96 DPI مباشرةً في الشيفرة. وهذا المقال هو نسخة WPF من ذلك. نترك النظريّة العامّة لتحجيم DPI الافتراضيّ (virtualization) وأوضاع الوعي بـ DPI لمقال WinForms، ونركِّز هنا على موضوع خاصّ بـ WPF: «لماذا ولأين تبقى المشكلة في WPF الذي يُفترَض أنّه يدعم DPI منذ البداية». نرتّب حسب ترتيب الاستخدام العمليّ: تشخيص الأعراض، وطريقة الإعلان عن دعم Per-Monitor DPI (لكلّ من .NET Framework 4.6.2 و.NET)، وحلول تلطّخ الخطوط والصور النقطيّة، ومطبّات مزج WindowsFormsHost، وحتّى جدول القرار بشأن مدى ما ينبغي فعله.

1. الخلاصة أوّلاً

  • يتابع WPF بشكل صحيح منذ البداية «دقّة DPI عند بدء التشغيل». فوحدة التخطيط هي وحدة مستقلّة عن الجهاز (DIP) مقدارها 1/96 من الإنش، ويُكيَّف نظام الرسم تلقائيّاً مع System DPI، فهو واعٍ بـ System DPI افتراضيّاً. لا حاجة لضبط أو فحص AutoScaleMode كما في WinForms.1
  • ومع ذلك، تنقسم المشكلات المتبقّية إلى 4 فئات: (أ) تلطّخ الشاشة كلّها عند النقل إلى شاشة بدقّة DPI مختلفة، (ب) ضبابيّة الصور النقطيّة والأيقونات، (ج) تلطّخ الخطوط وحدود الجداول، (د) المحتوى المُمزَج مثل WindowsFormsHost / WebBrowser. الأسباب والحلول مختلفة تماماً لكلّ منها، فابدأ بالتشخيص عبر جدول الفصل 3.
  • علاج (أ) الجذريّ هو دعم Per-Monitor DPI. ويدعم WPF على .NET Framework 4.6.2 فما بعده هذا كإطار عمل، وبمجرّد الإعلان عنه في البيان (manifest)، تُنفَّذ إعادة تحجيم النافذة نفسها تلقائيّاً.23
  • يبقى WPF على .NET (‏Core 3.1 إلى .NET 8) أيضاً واعياً بـ System DPI افتراضيّاً. ولا توجد في WPF آليّة مماثلة لـ ApplicationHighDpiMode / SetHighDpiMode في WinForms، وطريقة الإعلان هي البيان نفسه كما في .NET Framework.
  • (ج) مشكلة وضع فرعيّ للبكسل (sub-pixel) سابقة على DPI نفسه. والخطوة الأولى هي UseLayoutRounding="True" على العنصر الجذر، أمّا SnapsToDevicePixels فأداة أخرى للإلصاق وقت الرسم. القيمة الافتراضيّة لكليهما معطَّلة.45
  • (ب) اجعل الأصول المتّجهيّة مثل Path / Geometry الخيار الأوّل. وللأصول التي لا تتوفّر إلا كصور نقطيّة، جهِّز دقّات متعدّدة وبدِّلها بحسب DPI. الاستيفاء الافتراضيّ في WPF (‏Linear) هو الأكثر تسبّباً بالضبابيّة للصور الصغيرة كالأيقونات عند مقاييس غير صحيحة القسمة.6
  • (د) المحتوى المُمزَج هو العنصر الذي يحدِّد الحدّ الأعلى لدعم Per-Monitor. ويُذكَر صراحةً أنّ سلوك Per-Monitor لـ WPF عندما «يُوضَع» داخل ElementHost / HwndSource غير مدعوم رسميّاً.3
  • النظريّة العامّة لأوضاع الوعي بـ DPI (تحجيم DPI الافتراضيّ، والفرق بين System Aware وPer-Monitor V2، والتجاوز من جانب المستخدم عبر خصائص exe) وكيفيّة إعداد بيئة اختبار DPI مختلطة مشتركة مع الفصول 2-3 و7 من مقال WinForms، فلا نكرِّرها في هذا المقال.

2. لماذا WPF «محصَّن ضدّ DPI» ── DIP وSystem DPI Aware

جذر مشكلة الدقّة العالية في WinForms يكمن في أنّ الإحداثيّات والأحجام مكتوبة مباشرةً بالبكسل الفعليّ، وأنّه يحمل آليّة (AutoScale) أُضيفت لاحقاً لإعادة تحويل تخطيط مفترَض على 96 DPI وقت التشغيل. أمّا نقطة انطلاق WPF فمختلفة. الرقم 120 في Width="120" المكتوبة في XAML ليس بكسلاً فعليّاً، بل وحدة مستقلّة عن الجهاز (DIP) تجعل 1/96 من الإنش يساوي 1. يُحسَب التخطيط كلّه بهذه الوحدة، ويُحوَّل وقت الرسم إلى معامل يتناسب مع System DPI (‏1.5 ضعف في حالة 150%). وتُرسَم النصوص أيضاً كخطوط متّجهيّة، فلا يحدث تدهور شبيه بالصور النقطيّة عند التكبير. وبفضل هذه الآليّة، يعمل تطبيق WPF واعياً بـ System DPI دون أيّ إعلان.1

نضع فيما يلي جدول فروق مع WinForms.

  WinForms WPF
وحدة الإحداثيّات والحجم بكسل فعليّ DIP (‏1/96 من الإنش)
عند عدم الإعلان عن شيء Unaware (ضبابيّة بتكبير نظام التشغيل للصورة) System Aware (واضح على الشاشة الرئيسيّة)
متابعة DPI عند بدء التشغيل إعادة حساب على مستوى النموذج عبر AutoScaleMode. يلزم ضبط وفحص تحجيم تلقائيّ من نظام الرسم. لا حاجة لضبط
النموذج النمطيّ للانهيار عند DPI العالي تراكب النسخ المتراكبة وقصّها في التخطيط ذي الإحداثيّات الثابتة لا ينهار التخطيط، بل يظهر كتلطّخ وضبابيّة
حوادث ناتجة عن المصمِّم (Designer) إعادة كتابة AutoScaleDimensions (شائعة) لا يحدث من حيث المبدأ (تُحفَظ XAML بوحدة DIP كما هي)

كما يوضّح الجدول، فإنّ العمل الذي كان محور الجهد في WinForms - «إصلاح انهيار التخطيط» و«منع حوادث المصمِّم» - لا يكاد يحدث في WPF. فمقولة «WPF محصَّن ضدّ DPI» تصوّر صحيح.

غير أنّ التلقائيّة تقتصر على نطاق System Aware فقط. وSystem Aware وضع «التكيّف مع دقّة DPI الخاصّة بالشاشة الرئيسيّة عند تسجيل الدخول»، ولا يشمل المتابعة (Per-Monitor) لبيئة تختلف فيها DPI من شاشة إلى أخرى. كما أنّ ما يُكبَّر بوحدة DIP هو فقط ما يرسمه WPF بنفسه (النصوص، الأشكال، عناصر التحكّم)، بينما تصبح الصور النقطيّة ضبابيّة عند تكبيرها، والمحتوى المُمزَج الذي يحمل HWND يقع خارج نطاق تحجيم WPF. والاستشارات من نوع «كان يُفترَض أن يكون محصّناً ضدّ DPI» تحدث في الغالب في هذا الجزء المتبقّي بالذات.

3. مشكلات تحدث رغم ذلك ── تشخيص السبب من العرض

هذا هو جدول التشخيص الأوّل الذي نستخدمه عند تلقّي استشارة. وبما أنّ التطابق بين الأعراض والأسباب في WPF أوضح حتّى من WinForms، يمكن تحديد السبب تقريباً من هذا الجدول وحده.

العرض السبب العلاج
تلطّخ منتظم للشاشة كلّها عند نقلها إلى شاشة بدقّة DPI مختلفة، ويُصلَح بالعودة إلى الشاشة الأصليّة ما زال System Aware. نظام التشغيل يكبِّر النافذة كلّها كصورة نقطيّة الفصل 4 (التحوّل إلى Per-Monitor)
تلطّخ مباشرةً بعد تغيير إعداد التحجيم، أو عند اتّصال RDP من عميل بدقّة DPI عالية السبب نفسه أعلاه (لأنّ System DPI يُثبَّت عند تسجيل الدخول) الفصل 4
النصوص واضحة لكن الأيقونات والصور وحدها ضبابيّة أو صغيرة تكبير الأصول النقطيّة بالاستيفاء القسم 5.3
خطّ حدود أو إطار بعرض 1px يختلف سُمكه من مكان لآخر، أو يتلطّخ قليلاً وضع فرعيّ للبكسل وتنعيم الحواف القسم 5.1
تظهر النصوص الصغيرة متلطّخة على شاشة بدقّة 100% (‏96 DPI) تنعيم حواف تنسيق النصّ الافتراضيّ (Ideal) القسم 5.2
تصبح الصور المصنوعة ذاتيّاً (‏WriteableBitmap، RenderTargetBitmap، إلخ) ضبابيّة حساب حجم البكسل على افتراض 96 DPI الفصل 6
ينزاح موضع النافذة أو إحداثيّات الفأرة أو إحداثيّات لقطة الشاشة خلط بين DIP والبكسل الفعليّ الفصل 6
داخل WindowsFormsHost / WebBrowser / بعض عناصر تحكّم الجهات الخارجيّة فقط صغير أو خشن أو منهار محتوى مُمزَج (خارج نطاق تحجيم WPF) الفصل 7

نضيف توضيحاً بشأن السطر الأوّل «تلطّخ منتظم للشاشة كلّها». هذه حالة يعمل فيها تحجيم DPI الافتراضيّ (تكبير نظام التشغيل للصورة النقطيّة) المذكور في الفصل 2 من مقال WinForms على تطبيق واعٍ بـ System DPI، وليس عطلاً في التطبيق. فالتطبيق الواعي بـ System DPI يرسم بـ«دقّة DPI الخاصّة بالشاشة الرئيسيّة عند تسجيل الدخول»، وفي الشاشات ذات الدقّة المختلفة يكبِّر نظام التشغيل أو يصغِّر لموازنة الفرق. هذا إجراء إنقاذيّ من نظام التشغيل: لا ينهار التخطيط، لكنّه يتلطّخ بدلاً من ذلك.1 والإصلاح يعني قطع هذا الإنقاذ والإعلان عن «سأتابع بنفسي دقّة DPI الخاصّة بكلّ شاشة»، أي التحوّل إلى Per-Monitor.

وعلى العكس، يمكن إصلاح الأعراض من السطر الثالث فما بعده حتّى مع البقاء على System Aware. فإن كانت الاستشارة من نوع «لا يوجد تشغيل متعدّد الشاشات، لكنّ الأيقونات وخطوط الحدود قذرة على شاشة 150%»، يمكن تخطّي الفصل 4 والبدء من الفصل 5 مباشرةً.

4. تلطّخ الشاشات المتعدّدة ── دعم Per-Monitor DPI

4.1 حدود System Aware

يُثبَّت System DPI عند تسجيل الدخول بدقّة DPI الخاصّة بالشاشة الرئيسيّة. فإن كانت شاشة الحاسوب المحمول المدمجة بدقّة 150% هي الشاشة الرئيسيّة، يرسم WPF كلّ النوافذ بمعامل 1.5، وعند النقل إلى شاشة خارجيّة بدقّة 100% يعرضها نظام التشغيل مصغَّرة إلى 2/3. ويبدو هذا التحجيم من نظام التشغيل ضبابيّاً بوجه خاصّ عند النسب غير الصحيحة القسمة.1 ويمكن إدراك بالتجربة أنّ التدهور البصريّ في تركيبة غير مستوية مثل 125% و150% أكبر من فرق الضعفين بين 200% و100%.

بمعنى آخر، تقتصر المشكلة في WPF الواعي بـ System DPI على الاستخدام المتنقّل بين شاشات بدقّات DPI مختلفة، والحالات التي يتغيّر فيها إعداد التحجيم بعد تسجيل الدخول (كتغيّر الشاشة الرئيسيّة بفصل محطّة الإرساء وتوصيلها، أو دخول دقّة DPI الخاصّة بالعميل عبر RDP، وما شابه). وإن كان تطبيقاً داخليّاً يستخدمه الجميع على سطح مكتب ثابت بشاشة واحدة، يكتمل الأمر عمليّاً بالبقاء على System Aware. سنعود إلى هذا القرار في جدول الفصل 8.

4.2 طريقة الإعلان في .NET Framework 4.6.2 فما بعده

دُمج دعم Per-Monitor DPI في WPF ضمن .NET Framework 4.6.2.2 وقبل ذلك، حتّى لو أُعلِن عن Per-Monitor لنظام التشغيل، لم يكن جانب WPF يتابع تغيّر DPI، وكان يلزم كتابة إعادة تحجيم النافذة كلّها يدويّاً (ولهذا السبب كانت أمثلة عصر Windows 8.1 مبنيّة بشكل كبير باستخدام DLL مساعدة أصليّة (native helper DLL)). وابتداءً من 4.6.2، يعالج WPF رسالة WM_DPICHANGED تلقائيّاً، ويتولّى ضبط حجم النافذة وإعادة التخطيط وإعادة الرسم.

المتطلّبان اثنان: نظام تشغيل Windows 10 Anniversary Update (‏1607) فما بعده، وبناء (build) يستهدف .NET Framework 4.6.2 فما بعده.3 يُكتَب الإعلان في بيان التطبيق (application manifest).

<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
  <asmv3:windowsSettings>
    <!-- .NET Framework 4.6.2 إلى 4.7.2 يُعلَن عنه بـ PerMonitor منفردة (كما في دليل المطوّرين).
         دعم WPF لـ PerMonitorV2 يبدأ من .NET Framework 4.8 فما بعده،
         لذا في 4.8+ / .NET، ضَع V2 في المقدّمة بكتابة "PerMonitorV2, PerMonitor" -->
    <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings"
      >PerMonitor</dpiAwareness>
    <!-- لأنظمة التشغيل القديمة التي لا تتعرّف على dpiAwareness (رجوع احتياطيّ إلى System Aware) -->
    <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
  </asmv3:windowsSettings>
</asmv3:application>

لهذا البناء المزدوج معنى. يُتعرَّف على عنصر dpiAwareness ابتداءً من Windows 10 1607، ويُستخدَم أوّل قيمة يُتعرَّف عليها من بين القيم المفصولة بفاصلة.7 وفي أنظمة التشغيل القديمة التي لا تعرف عنصر dpiAwareness نفسه، يحدث رجوع احتياطيّ إلى إعلان dpiAware (‏System Aware)، وهذا هو المنطق.

انتبه هنا إلى نقطة التمييز بين استخدام PerMonitor وPerMonitorV2 بحسب هدف بناء الإطار. فـ دعم WPF الرسميّ لـ PerMonitorV2 (وMixed-Mode DPI) يبدأ من .NET Framework 4.8 فما بعده8، وحتّى مثال دليل مطوّري 4.6.2 هو إعلان PerMonitor منفردة.3 فإن كتبتَ V2 في المقدّمة مع هدف بناء 4.6.2 إلى 4.7.2، سيختار نظام التشغيل Windows 10 1703 فما بعده وضع V2 غير المدعوم من الإطار، فينتج سلوك غير متوقَّع خصوصاً حول المحتوى المُمزَج كـ WindowsFormsHost. وننصح بـالترقية إلى 4.8 فما بعده (أو WPF على .NET) أوّلاً، ثمّ وضع V2 في المقدّمة بكتابة PerMonitorV2, PerMonitor، وترك تحجيم المناطق غير الخاصّة بالعميل (non-client) كشريط العنوان وشريط التمرير لنظام التشغيل (راجع الفصل 3 من مقال WinForms).

توجد مطبّة واحدة تتعلّق بهدف البناء. فحتّى لو كان .NET Framework الخاصّ ببيئة التشغيل 4.6.2 فما بعده، إن بقي هدف المشروع 4.6.1 فما قبله، تكون متابعة Per-Monitor معطَّلة افتراضيّاً. في هذه الحالة، فعِّلها صراحةً عبر مفتاح AppContext في app.config.3

<configuration>
  <runtime>
    <!-- انتبه للنفي المزدوج: ضبط "عدم التحجيم عند تغيّر DPI" على false = تفعيل -->
    <AppContextSwitchOverrides value="Switch.System.Windows.DoNotScaleForDpiChanges=false"/>
  </runtime>
</configuration>

سبب الاستفسارات من نوع «كتبتُ البيان لكنّه لا يعمل» يعود بحسب التجربة تقريباً إلى إحدى حالتين: إمّا هدف البناء هذا، أو أنّ البيان غير مُدرَج في البناء (بقاء إعدادات المشروع على البيان الافتراضيّ).

4.3 طريقة الإعلان في .NET (‏Core 3.1 إلى .NET 8)

يبقى WPF على .NET أيضاً واعياً بـ System DPI افتراضيّاً بلا بيان. وفي مقابل امتلاك WinForms مدخلاً مخصّصاً في ملفّ المشروع مثل ApplicationHighDpiMode أو Application.SetHighDpiMode، لا توجد في WPF آليّة رسميّة لتبديل وضع الوعي بـ DPI من الشيفرة أو إعدادات المشروع. طريقة الإعلان هي نفسها في .NET Framework: أضِف app.manifest إلى المشروع (من Visual Studio: «إضافة عنصر جديد» ← «ملفّ بيان التطبيق»)، واكتب إعلان dpiAwareness نفسه الوارد في القسم 4.2، وأشِر إليه من ملفّ المشروع.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <UseWPF>true</UseWPF>
    <ApplicationManifest>app.manifest</ApplicationManifest>
  </PropertyGroup>
</Project>

بما أنّ بيئة التشغيل حديثة، فلا حاجة إلى مفتاح AppContext الوارد في القسم 4.2. انتبه فقط إلى أنّ «الانتقال إلى .NET» لا يعني «حلّ مشكلة الدقّة العالية تلقائيّاً». فسهولة الانتقال تخصّ جانب WinForms (القسم 4.1 من مقال WinForms)، أمّا WPF فقد بلغ منذ .NET Framework 4.6.2 المستوى نفسه الذي هو عليه الآن.

يبقى بعد الإعلان عمل ينبغي إنجازه كما في WinForms، لكنّ المحتوى مختلف. في WinForms كانت ساحة المعركة الرئيسيّة هي إصلاح انهيار التخطيط. أمّا في WPF فيتابع الإطار التخطيط بنفسه، فيبقى فحص عرض الشاشات كلّها، وتبديل الأصول النقطيّة بحسب DPI (القسم 5.3)، ومتابعة الشيفرة التي تتعامل مع البكسل مباشرةً (الفصل 6)، والتحقّق من المحتوى المُمزَج (الفصل 7). وبعدد شاشات متساوٍ، عادةً ما يكون إجمالي جهد التحوّل إلى Per-Monitor أصغر بدرجة من WinForms.

5. تلطّخ الرسم وضبابيّته ── الخطوط المفردة والنصوص والصور النقطيّة

محتوى هذا الفصل مستقلّ عن التحوّل إلى Per-Monitor. فهو يعمل حتّى مع البقاء على System Aware، لذا يستحقّ التطبيق حتّى في التطبيقات التي لا تعمل بشاشات متعدّدة.

5.1 تلطّخ الخطوط المفردة وحدود الجداول ── UseLayoutRounding وSnapsToDevicePixels

التخطيط بوحدة DIP يعني أنّ حدود العناصر لا تقع بالضرورة على مواضع صحيحة من البكسل الفعليّ. ففي 125%، يصبح 1 DIP = 1.25px، فيكون خطّ حدود بعرض 1 DIP معادلاً لـ 1.25 بكسل فعليّ، وتُرسَم الحافّة التي تقع في منتصف بكسل شبه شفّافة بتنعيم الحواف. والنتيجة هي «تلطّخ الخطّ» و«اختلاف السماكة الظاهرة من سطر لآخر رغم أنّ التحديد 1px نفسه في الكلّ». وحتّى عند 96 DPI، يحدث الأمر نفسه إن وقع الحساب على 0.5px بسبب قسمة أحجام النجمة (*) في Grid أو حساب هوامش التوسيط. هذه خاصّية رسم فرعيّ للبكسل في WPF، وليست مشكلة DPI بحدّ ذاتها، بل مشكلة «يسهل ظهورها كلّما ارتفعت DPI».4

الخطوة الأولى هي تقريب التخطيط. أضِف سطراً واحداً إلى العنصر الجذر.

<Window x:Class="MyApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        UseLayoutRounding="True">

UseLayoutRounding آليّة تُقرِّب قيم البكسل غير الصحيحة في مسار التخطيط، وهي معطَّلة افتراضيّاً، وإن ضُبِطت على الجذر تنتشر إلى الشجرة البصريّة كلّها.4 أمّا SnapsToDevicePixels المشابه في الاسم فدوره مختلف: لا يعمل على التخطيط، بل يُلصق الحواف بحدود البكسل وقت الرسم. وهو معطَّل افتراضيّاً (false) أيضاً، ويُوَرَّث الضبط إلى الشجرة الفرعيّة. وتذكر الوثائق الرسميّة استخدامه في تخفيف التلطّخ الناتج عن تنعيم الحواف حول الخطوط المفردة في بيئات تتجاوز 96 DPI.5

  UseLayoutRounding SnapsToDevicePixels
توقيت العمل مسار التخطيط (تقريب نتائج Measure / Arrange إلى بكسل صحيح) وقت الرسم (إلصاق الحواف بحدود البكسل)
القيمة الافتراضيّة false false
نطاق التطبيق ينتشر إلى العناصر السليلة إن ضُبِط على الجذر يُوَرَّث إلى العناصر السليلة أيضاً
الأثر الرئيسيّ التخطيط عموماً. انزياح 0.5px في حجم العنصر وموضعه، وما ينتج عنه من تلطّخ الخطوط والحدود تلطّخ متبقٍّ لعناصر فرديّة مثل Border والخطوط المفردة
معيار الاستخدام ضَعه أوّلاً على النافذة الجذر. للتطبيقات الجديدة، اجعله في نمط مشترك لكلّ النوافذ طبِّقه بدقّة على المواضع المتبقّية بعد UseLayoutRounding

إن ترددتَ، فالترتيب هو: «UseLayoutRounding="True" على الجذر، ثمّ SnapsToDevicePixels="True" على ما يبقى». وقد يكون من الآثار الجانبيّة للتقريب أن تصبح عروض الأعمدة المقسَّمة بالتساوي عبر أحجام النجمة غير متساوية بمقدار 1px، لكن نادراً ما شكّل هذا مشكلة في شاشات تطبيقات الأعمال حسب تجربتنا. أمّا تلطّخ الشيفرة التي ترسم ذاتيّاً عبر DrawingContext، فتوجد له أيضاً أداة منخفضة المستوى تسمّى GuidelineSet، لكن تقريب إحداثيّات الرسم فقط يحلّ معظم الحالات.

5.2 تلطّخ النصوص ── TextFormattingMode

الشكوى القديمة «نصوص WPF باهتة أو متلطّخة» تتعلّق بوضع تنسيق النصّ أكثر من DPI. لتنسيق النصّ في WPF وضعان: Ideal (الافتراضيّ) الذي يضع الأحرف وفق المقاييس المثاليّة الأصليّة للخطّ، وDisplay الذي يضعها وفق مقاييس متوافقة مع GDI.9 وIdeal يمتاز بتباعد أحرف جميل ومرونة مع التحجيم، لكنّ رسم نصّ صغير نسبيّاً عند 96 DPI (‏100%) قد يبدو متلطّخاً بتنعيم الحواف. وضبط TextOptions.TextFormattingMode="Display" على الجذر يمنح إحساساً بالوضوح شبيهاً بـ WinForms.

لكنّ الأمر يعكس نفسه في بيئات الدقّة العالية. فـ Display تنسيق يتوافق مع شبكة بكسل 96 DPI، لذا قد تنخفض الجودة عكسيّاً تحت التحجيم. والفصل العمليّ هو: إن كان معظم المستخدمين على بيئة 100%، استخدم Display، أمّا في البيئة القياسيّة الحاليّة التي أصبح 125% فما فوقها السائد فيها، فابقَ على Ideal الافتراضيّ. ويمكن أيضاً تنفيذ تبديل إلى Display فقط عند 100%، لكنّ الشاشات التي تستحقّ هذا الجهد (كشاشات محرِّرات النصوص) محدودة.

5.3 ضبابيّة الصور والأيقونات ── تفضيل الأصول المتّجهيّة ودقّات متعدّدة

يضع WPF أيضاً الصور النقطيّة المحدَّدة في Image بوحدة DIP، ويكبِّرها بحسب DPI. فأيقونة بحجم 16×16px تُكبَّر بالاستيفاء إلى 24×24px في بيئة 150%، وتصبح ضبابيّة بوضوح مع خوارزميّة الاستيفاء الافتراضيّة (‏Linear).6 ومظهر «النصوص واضحة لكنّ الأيقونات وحدها باهتة» في تطبيقات WPF يعود تقريباً كلّه إلى هذا.

توجد 3 حلول مرتَّبة حسب الأولويّة.

  1. اجعلها أصولاً متّجهيّة (الخيار الأوّل). الأيقونات المحفوظة بصيغة Path / Geometry / DrawingImage تُرسَم واضحة عند أيّ DPI، ولا تحتاج شيفرة تبديل. وتتوفّر أدوات تصميم وأدوات تحويل من SVG إلى XAML بكثرة. يستحقّ الأمر أن يكون قاعدة أنّ أيقونات التطبيقات الجديدة تُحفَظ متّجهيّة منذ البداية. وخطوط الأيقونات مثل Segoe MDL2 Assets تمنح الأثر نفسه.
  2. بدِّل الصور النقطيّة متعدّدة الدقّات بحسب DPI. للأصول التي لا يمكن تحويلها إلى متّجهيّة كالصور الفوتوغرافيّة ولقطات الشاشة، جهِّز نسخاً لـ 96 / 120 / 144 / 192 DPI (‏16 / 20 / 24 / 32px مثلاً)، واختر بحسب DPI الحاليّة. ويوصي الدليل الرسميّ للمطوّرين أيضاً بتبديل الأصول بحسب DPI كعلاج للضبابيّة.1
  3. اختر الاستيفاء عبر RenderOptions.BitmapScalingMode. حلّ مخفِّف عندما لا يمكن زيادة عدد الأصول. عند مضاعفات صحيحة مثل 200%، يكون التكبير كنقاط بـ NearestNeighbor أكثر وضوحاً، بينما يناسب HighQuality (‏Fant) تصغير الصور الكبيرة.6

يُنفَّذ تبديل الخيار 2 عبر OnDpiChanged (متاح ابتداءً من .NET Framework 4.6.2) إن كان دعم Per-Monitor مُفعَّلاً بالفعل.

public partial class MainWindow : Window
{
    protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi)
    {
        base.OnDpiChanged(oldDpi, newDpi);
        // يُستدعى في كلّ مرّة يُنقَل فيها بين الشاشات أو يتغيّر التحجيم
        AppIcon.Source = IconAssets.SelectFor(newDpi.DpiScaleX);
    }
}

public static class IconAssets
{
    // اختر ما يناسب معامل التكبير من الأصول المجهَّزة بـ 16 / 24 / 32 بكسل
    public static BitmapImage SelectFor(double scale) => scale switch
    {
        <= 1.0 => Load("icon16.png"),
        <= 1.5 => Load("icon24.png"),
        _      => Load("icon32.png"),
    };

    private static BitmapImage Load(string name) =>
        new(new Uri($"pack://application:,,,/Assets/{name}"));
}

إن بقيت على System Aware، لا تتغيّر DPI بعد بدء التشغيل، فيكفي اختيارها مرّة واحدة عند بدء التشغيل (‏Loaded) عبر VisualTreeHelper.GetDpi(this). وقبل كتابة شيفرة التبديل، فإنّ إعادة طرح السؤال في كلّ مرّة: «ألا يمكن أصلاً جعل هذا الأصل متّجهيّاً؟» هو في النهاية أسهل الطرق صيانةً.

6. الشيفرة التي تتعامل مع البكسل ومتابعة تغيّر DPI

ما يخرج عن مظلّة التحجيم التلقائيّ لـ WPF هو الشيفرة التي تحسب البكسل الفعليّ بنفسها. توجد ثلاثة أنماط نمطيّة، وكلّها تظهر بشكل مزعج: «مثاليّ على جهاز التطوير (100%)، وضبابيّ أو منزاح فقط عند العميل (150%)».

الأوّل هو شيفرة تصنع مخزّناً مؤقّتاً للبكسل بنفسها، مثل WriteableBitmap / RenderTargetBitmap. فإن صُنِعت بعدد بكسل مساوٍ لحجم DIP، تُكبَّر بالاستيفاء 1.5 ضعف في بيئة 150% وتتلطّخ. يمكن الحصول على DPI الحاليّة من بنية DpiScale التي يعيدها VisualTreeHelper.GetDpi، فاجعل عدد البكسل بالبكسل الفعليّ، وضمِّن DPI بشكل صحيح في المخزّن المؤقّت.

// imageHost: العنصر الذي يعرض نتيجة الرسم (ActualWidth/Height بوحدة DIP)
DpiScale dpi = VisualTreeHelper.GetDpi(imageHost);
int pixelWidth  = (int)Math.Ceiling(imageHost.ActualWidth  * dpi.DpiScaleX);
int pixelHeight = (int)Math.Ceiling(imageHost.ActualHeight * dpi.DpiScaleY);

// لا تبنِه بقيمة ثابتة 96,96، بل مرِّر DPI الفعليّة (بذلك يصبح 1 بكسل فعليّ = 1 بكسل في المخزّن المؤقّت)
var bitmap = new WriteableBitmap(
    pixelWidth, pixelHeight,
    dpi.PixelsPerInchX, dpi.PixelsPerInchY,
    PixelFormats.Bgra32, null);

الثاني هو إحداثيّات الشاشة والتفاعل مع Win32. الإحداثيّات التي يعيدها PointToScreen، والإحداثيّات المستقبَلة عبر WM_MOUSEMOVE أو الخطّافات (hooks)، والإحداثيّات الممرَّرة إلى MoveWindow، كلّها بالبكسل الفعليّ، وإن خُلِطت مع DIP في جانب XAML تنزاح بمقدار 1.25 ضعف في بيئة 125%. استخدم مصفوفة CompositionTarget للتحويل.

var source = PresentationSource.FromVisual(this);
if (source?.CompositionTarget is { } target)
{
    // تحويل من DIP إلى بكسل فعليّ
    Point device = target.TransformToDevice.Transform(new Point(x, y));
    // تحويل من بكسل فعليّ إلى DIP
    Point dip = target.TransformFromDevice.Transform(devicePoint);
}

الثالث هو التخزين المؤقّت (caching). إن كنتَ تخزِّن مؤقّتاً صوراً نقطيّة أو قيم تخطيط أو نصوصاً منسَّقة صُنعت بتضمين DPI، فسيصبح ذلك بالياً في كلّ مرّة تُنقَل فيها النافذة بين الشاشات بعد التحوّل إلى Per-Monitor. أعِد إنشاءها عبر OnDpiChanged (أو حدث DpiChanged الخاصّ بالنافذة) كما في القسم 5.3. وهنا أيضاً، إن بقيتَ على System Aware، فإنّ DPI ثابتة عند بدء التشغيل، فلا حاجة لشيفرة متابعة. وتقدير «تكلفة التحوّل إلى Per-Monitor = عدد الشيفرات التي تتعامل مع البكسل» يتوافق جيّداً مع الواقع.

علماً بأنّ التعامل مع خيط الواجهة (UI thread) عند تنفيذ معالجة إعادة الإنشاء هذه بشكل غير متزامن هو كما نظّمناه في «async و UI thread في WPF/WinForms».

7. مطبّات المحتوى المُمزَج ── WindowsFormsHost وWebBrowser والجهات الخارجيّة

يقتصر امتداد التحجيم التلقائيّ لـ WPF على ما يرسمه WPF بنفسه. العناصر التي تحمل HWND تقع «خارج» رسم WPF، فيصبح التعامل مع DPI أكثر تعقيداً بدرجة. وتذكر الوثائق الرسميّة لـ Windows أيضاً صراحةً: «الإطار الآخر الموضوع داخل WPF، وWPF الموضوع داخل إطار آخر، لا يُحجَّمان تلقائيّاً».10

شكل المزج ما الذي يحدث التعامل الواقعيّ
WindowsFormsHost (وضع WinForms داخل WPF) يحوِّل المضيف بين نظامي إحداثيّات DIP والبكسل الفعليّ، لكنّ تحجيم المحتوى الداخليّ يبقى في حدود ما يستطيع عنصر تحكّم WinForms التعامل معه11 افحص دعم DPI لجانب WinForms الداخليّ (‏AutoScaleMode وغيره) وفق معيار مقال WinForms. لا تتوقّع متابعة Per-Monitor
عنصر تحكّم WebBrowser (محرّك IE) محتوى أصليّ بـ HWND منفصل. قد لا تتطابق نسبة التكبير مع تحجيم التطبيق يُعامَل كإرث (legacy). أضِفه إلى مواد النظر عند دراسة الانتقال إلى WebView2
ElementHost / HwndSource (وضع WPF داخل WinForms أو Win32) سيناريو Per-Monitor غير مدعوم رسميّاً3 صمِّم في حدود System Aware بما يتوافق مع وضع الوعي بـ DPI الخاصّ بالتطبيق المضيف
عناصر تحكّم من جهات خارجيّة مستوى الدعم يختلف من منتج لآخر. المنتجات غير الداعمة لـ Per-Monitor تحدِّد الحدّ الأعلى لتلك الشاشة اجرِ جرداً لحالة دعم المورِّد وإصداراته قبل اتّخاذ قرار التحوّل إلى Per-Monitor

الأكثر شيوعاً في العمل الفعليّ هو السطر الأوّل. فإن حوَّلتَ إلى Per-Monitor تطبيقاً بتشكيل «انتقل إلى WPF، لكن معاينة التقارير والرسوم البيانيّة وحدها لا تزال تحمل عناصر تحكّم WinForms / ActiveX قديمة عبر WindowsFormsHost»، سيتابع جزء WPF بشكل مثاليّ، بينما الجزء الداخليّ للمضيف وحده لا يتابع DPI بعد النقل، ويبقى صغيراً وخشناً. وتوجد آليّة على مستوى Win32 لاستضافة DPI مختلطة (‏SetThreadDpiHostingBehavior)، لكنّها ليست شيئاً يمكن استخدامه بسهولة من WPF، ونحن في شركتنا نبدأ بحسم الأمر على أنّ «الجزء المُمزَج يحدِّد الحدّ الأعلى لدعم Per-Monitor»، ونبدأ بإعداد قائمة المحتوى المُمزَج أوّلاً. ومواد القرار في اتّجاه إزالة المزج مجمَّعة في «كيف تختار بين WinForms وWPF وWinUI» و«الإبقاء على ActiveX/OCX أم تغليفه أم استبداله».

8. إلى أين تصل ── جدول القرار والتقدّم المرحليّ

في حالة WPF، الخياران الفعليّان اثنان فقط (لا وجود لخيار «لا تفعل شيئاً = Unaware» كما في WinForms، لأنّه واعٍ بـ System DPI حتّى بلا إعلان).

  البقاء على System Aware دعم Per-Monitor
المظهر واضح على الشاشة الرئيسيّة. يتلطّخ على شاشة بدقّة DPI مختلفة، أو بعد تغيير التحجيم، أو عبر RDP واضح على كلّ الشاشات
عمل الإعلان لا حاجة (افتراضيّ) إضافة البيان فقط (الفصل 4)
العمل المتبقّي بعد الإعلان ── فحص عرض الشاشات كلّها + تبديل الأصول النقطيّة بحسب DPI (القسم 5.3) + دعم DpiChanged للشيفرة المتعلّقة بالبكسل (الفصل 6) + التحقّق من المحتوى المُمزَج (الفصل 7)
العنصر الذي يحدِّد السقف ── WindowsFormsHost وWebBrowser وعناصر تحكّم الجهات الخارجيّة
الحالة المناسبة تطبيق داخليّ يركِّز على شاشة واحدة وسطح مكتب ثابت. تطبيق يحوي محتوًى مُمزَجاً كثيراً استخدام مختلط بين حاسوب محمول وشاشة خارجيّة، تطبيق رئيسيّ طويل العمر، منتج يُوزَّع على العملاء

محاور القرار هي نفسها المذكورة في الفصل 7 من مقال WinForms (عمر التطبيق، بيئة الاستخدام، ميزانيّة التعديل)، لكنّ الفرق هو أنّ التكلفة الحديّة للتحوّل إلى Per-Monitor في WPF صغيرة. الإعلان ملفّ واحد، والتخطيط يتابعه الإطار، والعمل المتبقّي يتناسب مع عدد الشيفرات المتعلّقة بالبكسل والأصول. وإن كان التطبيق قليل المحتوى المُمزَج، فإنّ عائد الاستثمار في الوصول إلى Per-Monitor أعلى بوضوح منه في WinForms. والطريقة التي نوصي بها فعليّاً في مشاريعنا تتألّف من ثلاث مراحل.

  1. تحسين الجودة مع البقاء على System Aware: UseLayoutRounding على الجذر، وتحويل الأيقونات إلى متّجهيّة أو دقّات متعدّدة، وإصلاح DPI في سلسلة WriteableBitmap. تُحلّ هنا كلّ المشكلات باستثناء تلطّخ الشاشات المتعدّدة، وحتّى إن قرِّر عدم التحوّل إلى Per-Monitor، يبقى هذا العمل أصلاً مفيداً.
  2. إعلان Per-Monitor والفحص: أضِف البيان، وشغِّل الشاشات كلّها في بيئة DPI مختلطة لاستخراج المواضع المُشكلة. والمشكلات التي تُكتشَف هنا ينبغي أن تُصنَّف ضمن أحد الفصول 5 إلى 7.
  3. تنفيذ متابعة تغيّر DPI: تبديل الأصول وإعادة إنشاء التخزين المؤقّت عبر OnDpiChanged. وفي هذه المرحلة يُحدَّد «إلى أين يُقبَل المحتوى المُمزَج».

يمكن استخدام بيئة الاختبار المذكورة في الفصل 7 من مقال WinForms كما هي (شاشتان بتحجيم مختلف، وتبديل الشاشة الرئيسيّة مع إعادة تسجيل الدخول، وتغيير التحجيم أثناء التشغيل، وRDP من عميل بدقّة DPI عالية).10 وما ينبغي التركيز عليه في WPF هو خطوط الحدود والأيقونات والرسم الذاتيّ عند النسب غير الصحيحة القسمة (125% / 150%)، وسلوك سحب النافذة عبر الشاشات. ومعرفة توزيع بيئات الاستخدام (نسبة المستخدمين الذين يعملون بشاشات متعدّدة) مسبقاً يجعل قرار الاستثمار ثابتاً دون تذبذب. تناولنا هذا المنظور أيضاً في «تصميم UX لتطبيقات Windows».

9. الخلاصة

بفضل DIP والتحجيم التلقائيّ، يكون WPF واعياً بـ System DPI منذ البداية، ولا يكاد وجود لـ«صراع انهيار التخطيط» الذي كان الساحة الرئيسيّة في WinForms. ومع ذلك، تبقى أربع فئات من المشكلات - تلطّخ الشاشات المتعدّدة (عدم دعم Per-Monitor)، وضبابيّة الصور النقطيّة، وتلطّخ الخطوط المفردة، والمحتوى المُمزَج - ولكلّ منها علاج مستقرّ.

  • تلطّخ الشاشات المتعدّدة: في WPF على .NET Framework 4.6.2 فما بعده أو .NET، يُحصَل على متابعة Per-Monitor تلقائيّاً بمجرّد إعلان البيان
  • الخطوط المفردة وحدود الجداول: الخطوة الأولى هي UseLayoutRounding="True" على الجذر، ثمّ SnapsToDevicePixels لما يبقى
  • الأيقونات: الأصول المتّجهيّة هي الخيار الأوّل، والصور النقطيّة تحتاج دقّات متعدّدة + تبديلاً بحسب DPI
  • الشيفرة التي تتعامل مع البكسل وحدها، كـ WriteableBitmap وإحداثيّات الشاشة، هي الهدف الحقيقيّ للتعديل. وعددها هو ما يحدِّد جهد التحوّل إلى Per-Monitor
  • WindowsFormsHost / WebBrowser / الجهات الخارجيّة هي ما يحدِّد سقف الدعم. اجرِ جرداً لها أوّلاً

حتّى التطبيقات التي توقّفت عند فكرة «لا بدّ أن يكون الأمر جيّداً لأنّه WPF»، غالباً ما يتبيّن من خلال هذا التنظيم أنّ المواضع التي تحتاج إصلاحاً محدودة العدد. إن ترددتَ في تحديد إلى أين يمكن إصلاح تطبيقك، أو في مسح الوضع الحاليّ بما فيه المحتوى المُمزَج، أو في تحديد مستوى الدعم المناسب، يمكننا مساعدتك.

مقالات ذات صلة

مجالات الاستشارة ذات الصلة

تتعامل شركة Komura Soft LLC مع دعم الدقّة العالية في تطبيقات WPF / WinForms (مسح الوضع الحاليّ، وقرار إمكانيّة التحوّل إلى Per-Monitor، وجرد المحتوى المُمزَج وتعديله)، والتحقيق في أسباب أعطال العرض المصاحبة لاستبدال الحواسيب أو إدخال شاشات 4K، واستشارات تجديد واجهة المستخدم.

المراجع

  1. Microsoft Learn، Developing a Per-Monitor DPI-Aware WPF Application. حول أنّ WPF واعٍ بـ System DPI افتراضيّاً، وآليّة التحجيم التلقائيّ عبر DIP، وأنّ النقل إلى شاشة بدقّة DPI مختلفة يجعل نظام التشغيل يحجِّم الصورة فتصبح ضبابيّة بوجه خاصّ عند النسب غير الصحيحة القسمة، وفكرة تبديل الأصول النقطيّة بحسب DPI.  2 3 4 5

  2. Microsoft Learn، What’s new in .NET Framework. حول تفعيل الوعي بـ Per-Monitor DPI في WPF على .NET Framework 4.6.2، ومفتاح Switch.System.Windows.DoNotScaleForDpiChanges، ودليل المطوّرين على GitHub.  2

  3. GitHub (microsoft/WPF-Samples)، Per Monitor DPI Developer Guide. حول متطلّبات Windows 10 Anniversary Update و.NET Framework 4.6.2 فما بعده، وصياغة dpiAwareness / dpiAware في البيان، وAppContextSwitchOverrides للأهداف الأقدم من 4.6.2، وعدم دعم تشكيل وضع WPF داخل HwndSource / ElementHost لـ Per-Monitor.  2 3 4 5 6

  4. Microsoft Learn، Layout - WPF. حول التخطيط بوحدة DIP ورسم فرعيّ للبكسل يسبِّب ضبابيّة الحواف، وأنّ تقريب التخطيط (UseLayoutRounding) معطَّل افتراضيّاً، وانتشاره إلى الشجرة البصريّة عند ضبطه على العنصر الجذر.  2 3

  5. Microsoft Learn، UIElement.SnapsToDevicePixels Property. حول أنّ القيمة الافتراضيّة false، وتوريث الضبط إلى الشجرة الفرعيّة عند ضبطه على الجذر، وإمكانيّة تخفيف التشوّه البصريّ الناتج عن تنعيم الحواف حول الخطوط المفردة في بيئات تتجاوز 96 DPI.  2

  6. Microsoft Learn، BitmapScalingMode Enum. حول أنّ الافتراضيّ (Unspecified) هو Linear، وخصائص خوارزميّات الاستيفاء HighQuality (‏Fant) وNearestNeighbor.  2 3

  7. Microsoft Learn، Setting the default DPI awareness for a process. حول أسبقيّة عنصر dpiAwareness (ابتداءً من Windows 10 1607) على dpiAware، وسلوك الرجوع الاحتياطيّ الذي يستخدم أوّل قيمة يُتعرَّف عليها من بين القيم المفصولة بفاصلة. 

  8. Microsoft Learn، What’s new in .NET Framework. حول إضافة دعم Per-Monitor V2 DPI Awareness وMixed-Mode DPI Scaling إلى WPF في .NET Framework 4.8، وتحسين التفاعل مع HWND المستضاف / WinForms، ومفتاح AppContext اللازم للتفعيل. 

  9. Microsoft Learn، TextFormattingMode Enum. حول وضعي تنسيق النصّ Ideal (المقاييس المثاليّة) وDisplay (المقاييس المتوافقة مع GDI). 

  10. Microsoft Learn، High DPI Desktop Application Development on Windows. حول جدول دعم Per-Monitor بحسب إطار واجهة المستخدم، وأنّ الإطار الآخر الموضوع داخل WPF وWPF الموضوع داخل إطار آخر لا يُحجَّمان تلقائيّاً، ونقاط الاختبار في بيئة DPI مختلطة.  2

  11. Microsoft Learn، Layout Considerations for the WindowsFormsHost Element. حول تحويل WindowsFormsHost بين نظامي إحداثيّات DIP والبكسل الفعليّ، وأنّ التحجيم يبقى في حدود ما يستطيع عنصر تحكّم Windows Forms الداخليّ التعامل معه. 

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

سمعتُ أنّ WPF لا يحتاج إلى دعم DPI، فلماذا يحدث التلطّخ؟
وحدة تخطيط WPF هي وحدة مستقلّة عن الجهاز (DIP) مقدارها 1/96 من الإنش، ويعمل افتراضيّاً بوصفه واعياً بـ System DPI، لذا يتابع بشكل صحيح دقّة DPI الخاصّة بالشاشة الرئيسيّة عند بدء التشغيل. لكن بما أنّ System DPI يُثبَّت عند تسجيل الدخول، فإنّ نقل النافذة إلى شاشة بدقّة DPI مختلفة يجعل نظام التشغيل يكبِّر أو يصغِّر النافذة كلّها كصورة نقطيّة لموازنة الفرق، فتتلطّخ الشاشة كلّها بانتظام. ويبرز التدهور بوجه خاصّ عند تركيبات غير صحيحة القسمة مثل 125% و150%. والعلاج الجذريّ هو الإعلان عن دعم Per-Monitor DPI في البيان (manifest)، وفي WPF على .NET Framework 4.6.2 فما بعده تُنفَّذ إعادة تحجيم النافذة تلقائيّاً.
لماذا يتلطّخ خطّ حدود بعرض 1px في WPF، أو تختلف سماكته من مكان لآخر؟
لأنّ التخطيط يتمّ بوحدة DIP، فلا يُضمَن أن تقع حدود العناصر على مواضع صحيحة من البكسل الفعليّ. ففي بيئة 125%، يصبح 1 DIP = 1.25px، وأيّ حافّة تقع في منتصف بكسل تُرسَم شبه شفّافة بتنعيم الحواف (anti-aliasing) فتتلطّخ. الخطوة الأولى هي ضبط UseLayoutRounding="True" على النافذة الجذر، إذ يُقرَّب مسار التخطيط القيم غير الصحيحة للبكسل وينتشر ذلك على الشجرة البصريّة كلّها. وللمواضع التي يبقى فيها الخلل، يُطبَّق SnapsToDevicePixels="True" بدقّة، وهو يُلصق الحواف بحدود البكسل عند الرسم. القيمة الافتراضيّة لكليهما معطَّلة.
لماذا تكون النصوص واضحة في WPF بينما تبدو الأيقونات وحدها ضبابيّة؟
لأنّ النصوص تُرسَم كخطوط متّجهيّة (vector fonts) فتبدو واضحة عند أيّ دقّة DPI، بينما تُوضَع الصور النقطيّة بوحدة DIP وتُكبَّر بالاستيفاء (interpolation) بحسب DPI. فأيقونة بحجم 16×16px تُكبَّر إلى 24×24px في بيئة 150%، وتصبح ضبابيّة بالاستيفاء الافتراضيّ (Linear). وترتيب أولويّات العلاج هو: (1) جعلها أصولاً متّجهيّة عبر Path أو Geometry أو DrawingImage أو خطوط الأيقونات، (2) تجهيز صور نقطيّة بدقّات متعدّدة وتبديلها بحسب DPI عبر VisualTreeHelper.GetDpi أو OnDpiChanged، (3) ضبط خوارزميّة الاستيفاء عبر RenderOptions.BitmapScalingMode.
كيف يُضبَط دعم Per-Monitor في WPF؟
يُعلَن عنه بكتابة عنصر dpiAwareness في بيان التطبيق (application manifest). ولا توجد في WPF وسيلة إعداد مشروع أو تبديل من الشيفرة مثل ApplicationHighDpiMode في WinForms. والمتطلّبات هي نظام تشغيل Windows 10 بإصدار 1607 فما بعده، وهدف بناء (target) .NET Framework 4.6.2 فما بعده، وإن أُعلِن عنه يتولّى الإطار تلقائيّاً كلّ شيء من معالجة WM_DPICHANGED حتّى إعادة تحجيم النافذة. ودعم WPF لـ PerMonitorV2 يبدأ من .NET Framework 4.8 فما بعده، لذا يُعلَن عنه في 4.6.2 إلى 4.7.2 بـ PerMonitor منفردة. وإن بقي هدف البناء 4.6.1 فما قبله، تبقى المتابعة معطَّلة، فيلزم تفعيلها عبر مفتاح AppContext. علماً بأنّ سلوك Per-Monitor في WPF عند وضعه داخل ElementHost أو HwndSource غير مدعوم رسميّاً.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

روابط عامة

العودة إلى المدونة