الوضع الداكن وسمة التباين في تطبيقات Windows ── شريط العنوان الداكن عبر DWM، وتتبع سمة النظام في WinForms/WPF، والرسم عند التباين العالي

· آخر تحديث: · · الوضع الداكن, سمة التباين, التباين العالي, DWM, WinForms, WPF, Windows 11, إمكانية الوصول, تطبيقات الأعمال, Windows

سجل التعديلات (النسخة الأولى، نُشرت في 2 Sep، 2026)
النشر الأول

«استبدلنا حواسيب الشركة بـ Windows 11 فجاء موظّفون على الوضع الداكن يقولون: تطبيق الأعمال عندنا وحده شريط عنوانه أبيض ناصع يؤذي العين»، «فعّل موظّف ضعيف البصر سمة التباين فاختفى عرض الحالة في شاشة الطلبات» ── عرضان ازدادت الاستشارة حولهما في السنة أو السنتين الأخيرتين.

الأوّل من إعداد «الألوان» في Windows 11، والثاني من إعداد «إمكانية الوصول»، لكن عين المطوّر تراهما المشكلة نفسها: «لا يتبع السمة». وفي الواقع أساس المعالجة مشترك. لا تثبّت الألوان برمجيّاً؛ اقرأ إعداد النظام، ولاحظ التغيير، وأعد الطلاء. هذه ثلاث نقاط.

المقالة السابقة «مدخل إلى إمكان الوصول في تطبيقات Windows» تناولت الآلية التي تقرأ بها قارئات الشاشة (UI Automation) وأساسيات التسمية ولوحة المفاتيح واللون. لمست فيها تتبع سمة التباين، لكنّها لم تتناول وضع اللون الفاتح/الداكن نفسه. هذه المقالة أختها، وتربط محورين: وضع اللون (فاتح/داكن) وسمة التباين، على مستوى التنفيذ، بهذا الترتيب: رسم شريط العنوان عبر DWM (مدير نوافذ سطح المكتب)، وتتبع سمة النظام في WinForms/WPF، والرسم تحت سمة التباين. القرّاء المستهدفون مطوّرون يطوّرون ويصونون تطبيقات أعمال بـ WinForms/WPF/Win32، البيئة المفترضة Windows 11 (شريط العنوان الداكن من الإصدار 22000 فما بعده) وWinForms وWPF على .NET 9/10 (على .NET Framework 4.8 و.NET 8 يُنفَّذ بعضه يدوياً)، الصعوبة متوسّطة.

مسار هذه المقالةمن ترتيب محوري السمة، إلى سبب البقاء فاتحاً افتراضيّاً، فشريط العنوان الداكن عبر DWM، فآلية الكشف والتتبع، فتنفيذ WinForms وWPF، فالرسم تحت سمة التباين، فاختيار السياسة والتحقّق، بهذا الترتيبترتيب محوري السمةسبب البقاء فاتحاً افتراضيّاًشريط العنوان الداكن عبر DWMآلية الكشف والتتبعتنفيذ WinForms/WPFالرسم تحت سمة التبايناختيار السياسة والتحقّق

الشكل 1: تربط هذه المقالة ترتيب السمة بالآلية والتنفيذ وسمة التباين والتحقّق في مسار واحد.

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

  • «سمة» Windows محوران. الفاتح/الداكن (وضع اللون) تحت «الإعدادات > التخصيص > الألوان»، و«الإعدادات > إمكانية الوصول > سمات التباين». الثانية لوحة مقيَّدة بنسبة تباين تبلغ تقريباً 7:1 فما فوق، وهي غير الفاتح/الداكن. ما دامت سمة التباين مفعّلة لا يتاح الوضع الداكن. أولوية الحكم «سمة التباين ← الفاتح/الداكن».12
  • بقاء شريط عنوان التطبيق القائم أبيض افتراض للتوافق. Windows لا يملك وسيلة لمعرفة هل التطبيق يدعم الداكن، فيعامل كلّ النوافذ فاتحة افتراضيّاً.3
  • تغميق شريط العنوان هو DWMWA_USE_IMMERSIVE_DARK_MODE (القيمة 20) لـ DwmSetWindowAttribute. تمرير TRUE من BOOL يرسم الإطار داكناً عندما يكون النظام داكناً. الدعم الموثَّق Windows 11 الإصدار 22000 فما بعده.43
  • الوضع الحالي يُقرأ بـ UISettings.GetColorValue، والتغيير يُستقبل بـ ColorValuesChanged. حكم Microsoft الرسمي: إن كان لون المقدّمة (لون النصّ الافتراضي) ساطعاً فالوضع داكن. لا ضمان لوصول الحدث على خيط الواجهة، فارجع إلى الواجهة ثمّ أعد الطلاء.35
  • في WinForms دخل Application.SetColorMode في .NET 9 ولم يعد تجريبيّاً في .NET 10. استدعِ SystemColorMode.System قبل Application.Run. القيود الثلاثة: Windows 11 فقط، معطّل أثناء سمة تباين، لا يتبع تغيّر الإعداد أثناء التشغيل.672
  • في WPF دخلت سمة Fluent وThemeMode في .NET 9. يتبع بـ ThemeMode="System" ويُتحكَّم أيضاً في تغميق النافذة. لكن التشغيل من الشيفرة تجريبي حتّى في .NET 10 (WPF0001)، ونمط Fluent «قيد العمل». إن بقيت على السمة التقليدية فبدّل ResourceDictionary للفاتح/الداكن بـ DynamicResource.8910
  • تحت سمة التباين انقل الألوان إلى أزواج ألوان النظام الصحيحة، واحذف الصور خلف النصّ، وارسم الرسوم متعدّدة الألوان بلوني المقدّمة والخلفية. الحكم بـ SPI_GETHIGHCONTRAST (في WinForms SystemInformation.HighContrast، وفي WPF SystemParameters.HighContrast)، والإشعار بـ WM_SYSCOLORCHANGE / WM_THEMECHANGED (في .NET SystemEvents.UserPreferenceChanged).111213
  • دعم الوضع الداكن ليس بديلاً عن دعم إمكانية الوصول. لوحة الداكن أيضاً تحتاج نسبة تباين 4.5:1، والإبلاغ الذي لا يعتمد على اللون وحده لازم في أيّ سمة.1415

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

في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 28، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle

2. «السمة» محوران ── الفاتح/الداكن وسمة التباين

2.1. الفاتح/الداكن (وضع اللون)

وضع اللون في «الإعدادات > التخصيص > الألوان» في Windows إعداد يحدّد سطوع لون المقدّمة ولون الخلفية لنظام التشغيل والتطبيق جملة. تعرّف وثائق Microsoft الفاتح بأنّه «خلفية ساطعة ومقدّمة داكنة»، والداكن بأنّه «خلفية داكنة ومقدّمة ساطعة»، وتضيف أن المقدّمة هنا «لون النصّ الافتراضي». في الوضع الداكن تصير المقدّمة (النصّ) ساطعة والخلفية داكنة.3

يُحفظ هذا الإعداد في قيمتي DWORD AppsUseLightTheme (وضع التطبيق) وSystemUsesLightTheme (وضع Windows نفسه) تحت HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize في السجلّ، وهما في مرجع إعدادات Microsoft.16 لكن كما سيأتي، المسار النظامي للقراءة من التطبيق هو UISettings في WinRT.

2.2. سمة التباين (التباين العالي)

سمة التباين المختارة تحت «الإعدادات > إمكانية الوصول > سمات التباين» تستخدم لوحة مقيَّدة لتبلغ نسبة تباين تقريباً 7:1 فما فوق، وهي لمن يحتاجون فصلاً بصريّاً قويّاً بين المقدّمة والخلفية. في Windows 11 أربعة أنواع مضمَّنة: Aquatic وDesert وDusk وNight sky، ويمكن للمستخدم لا الاختيار منها فقط بل تحرير ألوان الخلفية والنصّ والارتباط التشعّبي والنصّ المعطّل ونصّ التحديد والزرّ فرادى. يمكن التبديل سريعاً بـ Left Alt+Left Shift+PrintScreen، وإن لم يُختر شيء طُبِّق Aquatic.1

تنصّ وثائق Microsoft صراحة: «لا تخلط سمات التباين بسمات الفاتح/الداكن». الفاتح/الداكن يستخدم لوحة واسعة وليس محسَّناً لأقصى تباين.1 والمهمّ أن الوضع الداكن لا يتاح أثناء تفعيل سمة تباين. لا يقدّم Application.SetColorMode في WinForms الداكن أثناء سمة تباين، وRequestedTheme في XAML يتجاوزه النظام.217

2.3. أولوية الحكم

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

محورا السمة وأولوية الحكمإن كانت سمة التباين مفعّلة فأوكل جملة إلى أزواج ألوان النظام، وإن لم تكن فاقرأ وضع اللون الفاتح/الداكن واختر لوحة التطبيقمفعّلةغير مفعّلةفاتحداكنهل سمة التباين مفعّلة؟أوكل إلى أزواج ألوان النظاموضع اللون هو؟لوحة الفاتحلوحة الداكنالوضع الداكن غير متاح

الشكل 2: ضع حكم سمة التباين أوّلاً، ولا تختر لوحة الفاتح/الداكن إلّا عندما تكون غير مفعّلة.

3. لماذا تبقى التطبيقات القائمة بيضاء في الوضع الداكن

النافذة من منطقتين. منطقة غير العميل من شريط العنوان والإطار وأزرار التعليق، ومنطقة العميل التي يرسمها التطبيق. من Windows Vista فما بعده يركّب DWM (مدير نوافذ سطح المكتب) منطقة غير العميل ويرسمها، ويحدّد التطبيق سمات الرسم بـ DwmSetWindowAttribute.18

تشرح وثائق Microsoft سبب بقاء التطبيقات القائمة بيضاء بصراحة. «Windows لا يعرف هل يستطيع التطبيق دعم الوضع الداكن، فيفترض للتوافق العكسي أنّه لا يستطيع». هناك أطر تعامل الوضع الداكن أصلاً مثل WinUI وWindows App SDK، لكن تطبيقات Win32 في كثير من الحالات لا تدعم الوضع الداكن، فيعطي Windows افتراضيّاً شريط عنوان فاتحاً.3

منطقتا النافذة وجهة الرسممنطقة غير العميل من شريط العنوان والإطار يرسمها DWM، ومنطقة العميل يرسمها التطبيق أو إطار الواجهة، فدعم الداكن يلزم لكليهمانافذة المستوى الأعلىمنطقة غير العميل (شريط العنوان والإطار)منطقة العميل (محتوى الشاشة)يركّبها DWM ويرسمهايرسمها التطبيق أو الإطاريُشار إليها بـ DwmSetWindowAttributeلوحة التطبيق نفسه

الشكل 3: شريط العنوان يرسمه DWM والمحتوى يرسمه التطبيق، فدعم الداكن يحتاج إشارة إلى DWM ولوحة التطبيق كليهما.

من هنا نتيجتان. الأولى أن تغميق شريط العنوان يحتاج أن يطلب التطبيق من DWM صراحة. والثانية أن ما يصير داكناً نتيجة الطلب شريط العنوان فقط، ومنطقة العميل يجب أن يعيد التطبيق طلاءها بنفسه. تقول الوثائق أيضاً «لدعم الوضع الداكن كاملاً يلزم أن يتبع سطح التطبيق كلّه السمة الداكنة»، وتقطع بأن الدليل الرسمي يعالج الكشف وشريط العنوان حتّى، ولا يعالج طريقة إعادة طلاء منطقة العميل.3 تطبيق شريط عنوانه أسود ومحتواه أبيض أشدّ نشازاً من تطبيق أبيض كما هو.

آلية البقاء فاتحاً افتراضيّاًWindows لا يعرف هل التطبيق يدعم الداكن فيجعله فاتحاً افتراضيّاً للتوافق، ولا يرسم الإطار وفق إعداد النظام الداكن إلّا عندما يمرّر التطبيق TRUE لسمة DWMلانعمWindows لا يعرف إمكان الدعمالافتراض فاتح للتوافقهل مرّر التطبيق TRUE؟إطار فاتح دائماًيُرسم وفق إعداد النظام

الشكل 4: Windows الذي لا يعرف إمكان الدعم يجعله فاتحاً افتراضيّاً، ولا يتبع إلّا عند إشارة صريحة من التطبيق.

4. شريط العنوان الداكن عبر DWM ── DwmSetWindowAttribute

4.1. DWMWA_USE_IMMERSIVE_DARK_MODE

سمة تغميق شريط العنوان هي DWMWA_USE_IMMERSIVE_DARK_MODE. شرح تعداد DWMWINDOWATTRIBUTE هكذا. «السماح برسم إطار هذه النافذة بألوان الوضع الداكن عندما يكون إعداد النظام للوضع الداكن مفعّلاً. للتوافق تصير كلّ النوافذ وضعاً فاتحاً افتراضيّاً بغضّ النظر عن إعداد النظام. يشير pvAttribute إلى BOOL، فـ TRUE يحترم الوضع الداكن وFALSE يظلّ فاتحاً دائماً. مدعوم في Windows 11 الإصدار 22000 فما بعده».4

أي أن TRUE ليس «اجعله داكناً» بل إذن «إن كان النظام داكناً جاز جعله داكناً». إن كان جانب التطبيق جاهزاً لطلاء منطقة العميل داكنة، فتمرير TRUE وحده يجعل شريط العنوان يتبع إعداد النظام. وبالعكس، إن كان التصميم عرضاً فاتحاً دائماً («تثبيت الفاتح» الآتي) فلا بأس بالإبقاء على FALSE الافتراضي.

شيفرة C++ في الدليل الرسمي بهذا الشكل. تشمل حتّى إجراء تعريف القيمة 20 ذاتيّاً لأن الثابت قد لا يوجد في ترويسة SDK قديمة.3

#include <dwmapi.h>
#pragma comment(lib, "dwmapi.lib")

#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif

// هل هو Windows 11 (الإصدار 22000) فما بعده. يفترض بياناً
// supportedOS لـ Windows 10 فما بعده (وإلّا يُدوَّر الإصدار إلى ما يعادل Windows 8)
bool IsWindows11OrGreater()
{
    OSVERSIONINFOEXW osvi{ sizeof(osvi) };
    osvi.dwMajorVersion = 10;
    osvi.dwMinorVersion = 0;
    osvi.dwBuildNumber = 22000;
    DWORDLONG mask = 0;
    VER_SET_CONDITION(mask, VER_MAJORVERSION, VER_GREATER_EQUAL);
    VER_SET_CONDITION(mask, VER_MINORVERSION, VER_GREATER_EQUAL);
    VER_SET_CONDITION(mask, VER_BUILDNUMBER, VER_GREATER_EQUAL);
    return ::VerifyVersionInfoW(
        &osvi, VER_MAJORVERSION | VER_MINORVERSION | VER_BUILDNUMBER, mask) != FALSE;
}

// honorDarkMode = true: إن كان النظام داكناً جاز رسم شريط العنوان داكناً أيضاً
void ApplyTitleBarTheme(HWND hwnd, bool honorDarkMode)
{
    if (!IsWindows11OrGreater())
    {
        // الدعم الموثَّق من الإصدار 22000 فما بعده. دون ذلك لا تُستدعَ واتبع الافتراض (فاتح)
        LogInfo(L"DWMWA_USE_IMMERSIVE_DARK_MODE is not documented for this OS build; keeping the default light frame");
        return;
    }
    BOOL value = honorDarkMode ? TRUE : FALSE;
    HRESULT hr = ::DwmSetWindowAttribute(
        hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &value, sizeof(value));
    if (FAILED(hr))
    {
        // الفشل على نظام تشغيل مدعوم شذوذ. لا تبتلعه صامتاً؛ سجّل HRESULT وأظهره
        LogWarning(L"DwmSetWindowAttribute(DWMWA_USE_IMMERSIVE_DARK_MODE) failed: 0x%08X", hr);
    }
}

نضيف جملة عن سبب التقسيم بإصدار نظام التشغيل. الدعم الموثَّق Windows 11 الإصدار 22000 فما بعده.4 التقارير بأن القيمة نفسها تنفع في Windows 10 ليست نادرة، لكن لا ينبغي لتطبيق أعمال أن يعتمد عرضه على سلوك غير موثَّق. إن صمّمت «جرّب الاستدعاء وإن فشل فاتركه» نجح الاستدعاء في Windows 10 فصار شريط العنوان داكناً فوق سلوك غير موثَّق. دون الإصدار 22000 لا تُستدعَ واتبع الافتراض الموثَّق «شريط عنوان فاتح»، وعلى نظام تشغيل مدعوم سجّل HRESULT عند الفشل وأظهره، هذا فقط. إجراءات قديمة تستخدم القيمة 19، أو تستدعي تصدير ترتيب uxtheme.dll لتغميق التحكّمات المشتركة، شائعة على الإنترنت أيضاً، وكلّها واجهات غير عامّة ولا يضمن أحد تغيّر السلوك عند التحديث.

4.2. توقيت الاستدعاء ── عندما يكون HWND حيّاً، وكلّما أُعيد إنشاؤه

DwmSetWindowAttribute يُستدعى على HWND، فيجب أن يكون بعد إنشاء مقبض النافذة. ونموذج WinForms قد يُعاد إنشاء المقبض عند تغيّر ShowInTaskbar ونحوه. المقبض الجديد بعد إعادة الإنشاء لا يحمل السمة، فموضع الاستدعاء ليس «المنشئ» بل «موضع يُستدعى كلّما أُنشئ المقبض». في WinForms OnHandleCreated، وفي WPF SourceInitialized.

توقيت استدعاء DwmSetWindowAttributeاضبط سمة DWM بعد إنشاء مقبض النافذة، وأعد الضبط على المقبض الجديد إن أُعيد إنشاء المقبض، وأعد الضبط أيضاً عند إشعار تغيّر السمةإعادة إنشاء المقبضإشعار تغيّر السمةإنشاء HWNDضبط سمة DWMأثناء العرض

الشكل 5: سمة DWM مربوطة بـ HWND، فأعد ضبطها عند كلّ إنشاء وكلّ إعادة إنشاء.

P/Invoke في WinForms كما يلي (لكتابة DllImport الآمنة راجع «استدعاء Win32 API بأمان من C#»).

using System.Runtime.InteropServices;

public partial class MainForm : Form
{
    private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;

    [DllImport("dwmapi.dll")]
    private static extern int DwmSetWindowAttribute(
        IntPtr hwnd, int attribute, ref int value, int size);

    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);
        ApplyTitleBarTheme();
    }

    private void ApplyTitleBarTheme()
    {
        // الدعم الموثَّق من الإصدار 22000 فما بعده. دون ذلك لا تُستدعَ واتبع الافتراض (فاتح).
        // حكم يعمل أيضاً على .NET Framework. لكن على .NET Framework، إن لم يوجد بيان
        // supportedOS لـ Windows 10 فما بعده دُوِّر الإصدار إلى ما يعادل Windows 8
        // (على .NET 5 فما بعده يجوز OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000))
        if (Environment.OSVersion.Version < new Version(10, 0, 22000))
        {
            _logger.LogInformation("Dark title bar is not documented for this OS build; keeping the default light frame");
            return;
        }
        // 1(TRUE) = إن كان النظام داكناً جاز الرسم داكناً. 0(FALSE) = فاتح دائماً
        int honorDarkMode = 1;
        int hr = DwmSetWindowAttribute(
            Handle, DWMWA_USE_IMMERSIVE_DARK_MODE, ref honorDarkMode, sizeof(int));
        if (hr < 0)
        {
            _logger.LogWarning("DwmSetWindowAttribute failed: 0x{Hr:X8}", hr);
        }
    }
}

هذه الشيفرة لازمة لتطبيقات .NET 8 فما قبله و.NET Framework وWin32/MFC. إن استخدمت Application.SetColorMode في WinForms على .NET 9 فما بعده، أو ThemeMode في WPF على .NET 9 فما بعده، يتولّى الإطار تغميق النافذة (وثائق ThemeMode تنصّ على أنّه «يتحكّم أيضاً في تطبيق مادّة الخلفية والوضع الداكن على النافذة»).10 الاستدعاء المزدوج لا يضرّ، لكن مصدر المسؤولية يلتبس فاختر أحدهما.

في WPF يتحدّد HWND عند SourceInitialized. خذ المقبض من WindowInteropHelper.19

using System.Windows.Interop;

public partial class MainWindow : Window
{
    protected override void OnSourceInitialized(EventArgs e)
    {
        base.OnSourceInitialized(e);
        var hwnd = new WindowInteropHelper(this).Handle;
        TitleBarTheme.Apply(hwnd, honorDarkMode: true); // المحتوى P/Invoke السابق
    }
}

4.3. لون شريط العنوان ولون النصّ ولون الإطار ومادّة الخلفية

في Windows 11 أُضيفت أيضاً سمات تحدّد لون شريط العنوان نفسه، لا خيار الداكن/الفاتح وحده.

السمة القيمة المحتوى إصدار الدعم
DWMWA_USE_IMMERSIVE_DARK_MODE 20 إن كان النظام داكناً ارسم الإطار داكناً (BOOL) 22000
DWMWA_BORDER_COLOR 34 لون إطار النافذة (COLORREF). يمكن إخفاء الإطار بـ DWMWA_COLOR_NONE 22000
DWMWA_CAPTION_COLOR 35 لون شريط العنوان (COLORREF) 22000
DWMWA_TEXT_COLOR 36 لون نصّ العنوان (COLORREF) 22000
DWMWA_SYSTEMBACKDROP_TYPE 38 مادّة خلفية يرسمها النظام (Mica أو Acrylic) 22621

السمات الثلاث التي تحدّد لوناً تعود إلى افتراض النظام عند تمرير DWMWA_COLOR_DEFAULT (0xFFFFFFFF). للون الإطار انتبه إلى أن «تغيير اللون وفق تغيّر الحالة مثل تنشيط النافذة مسؤولية التطبيق».4 مادّة الخلفية تُحدَّد بتعداد DWM_SYSTEMBACKDROP_TYPE، وDWMSBT_MAINWINDOW يعادل Mica في Windows 11 وDWMSBT_TRANSIENTWINDOW يعادل Acrylic، لكنّه منصوص أن «أثر المادّة قد يتغيّر في Windows لاحق».20

موضع الاستخدام في تطبيق أعمال فكّر فيه بحذر. طلاء شريط العنوان بلون العلامة يولّد مسؤولية ضمان تباين نصّ العنوان وأزرار التعليق فوق ذلك اللون بنفسك. إلى حالتي الداكن/الفاتح تُضاف توليفة نشط/غير نشط. الصواب لكثير من تطبيقات الأعمال «اتبع افتراض النظام (TRUE للقيمة 20 فقط)»، ولون العلامة خيار عندما يلزم حقّاً.

كيف تقرّر لون شريط العنوانإن اتبعت افتراض النظام كفت TRUE للقيمة 20، وإن طليت بلون العلامة صارت مسؤولية ضمان تباين النصّ وأزرار التعليق وإدارة لون النشط/غير النشط على التطبيق، وللإرجاع مرّر DWMWA_COLOR_DEFAULTاتبع افتراض النظاماطلِ بلون العلامةلون شريط العنوان هو؟TRUE للقيمة 20 فقطحدّد سمات اللون (34–36)اضمن بنفسك تباين النصّ والأزرارأدر بنفسك النشط/غير النشطللإرجاع COLOR_DEFAULT

الشكل 6: اختيار لون العلامة ينقل مسؤولية التباين وإدارة الحالة إلى جهة التطبيق، وفي كثير من تطبيقات الأعمال الصواب اتباع الافتراض.

5. كشف سمة النظام وتتبعها ── اقرأ ولاحظ وأعد الطلاء

عمل جعل منطقة العميل تتبع ينقسم ثلاثة. اقرأ الإعداد الحالي، لاحظ التغيير، ثمّ أعد الطلاء.

ثلاث خطوات للكشف والتتبعحلقة تقرأ وضع اللون الحالي بـ UISettings عند التشغيل، وتلاحظ التغيير بإشعارات مثل ColorValuesChanged، وترجع إلى خيط الواجهة وتعيد تطبيق لوحة التطبيقاقرأ: UISettings.GetColorValueأعد الطلاء: أعد تطبيق اللوحةلاحظ: ColorValuesChangedارجع إلى خيط الواجهةأعد ضبط سمة DWM أيضاً

الشكل 7: حلقة القراءة عند التشغيل والملاحظة بالإشعار والرجوع إلى خيط الواجهة وإعادة الطلاء هيكل تتبع السمة.

5.1. اقرأ ── UISettings و«إن كانت المقدّمة ساطعة فالداكن»

الإجراء الرسمي من Microsoft يستخدم Windows.UI.ViewManagement.UISettings في WinRT. خذ لون المقدّمة (لون النصّ الافتراضي) بـ GetColorValue(UIColorType::Foreground)، وقدّر السطوع المدرَك بحساب صحيح واحكم «هل هو ساطع»، وإن كانت المقدّمة ساطعة فاحكم وضعاً داكناً. تقطع الوثائق بأن هذه المعادلة ليست نموذجاً صارماً لتحليل السطوع بل تقريباً كافياً لتصنيف الفاتح/الداكن.321

UISettings صنف WinRT، لكن يمكن استدعاؤه مباشرة من C# في WPF/WinForms إن جعلت TargetFramework يحمل إصدار Windows SDK مثل net8.0-windows10.0.19041.0 (الآلية في «WinRT هو COM»). هناك أيضاً قراءة AppsUseLightTheme في السجلّ مباشرة، لكن السجلّ موضع حفظ الإعداد لا عقد واجهة، فإن قرأت فاجعل UISettings الأصل والسجلّ للتشخيص.

مسار قراءة وضع اللونالمسار النظامي أخذ لون المقدّمة بـ UISettings في WinRT والحكم على السطوع، وAppsUseLightTheme في السجلّ موضع حفظ يُترك للتشخيصهل الحالي فاتح أم داكنUISettings.GetColorValueAppsUseLightTheme في السجلّاحكم على السطوع المدرَك للون المقدّمةإن كان ساطعاً فداكنموضع حفظ. اتركه للتشخيص

الشكل 8: مسار القراءة النظامي UISettings، والسجلّ موضع حفظ فحسب.

5.2. لاحظ ── ColorValuesChanged لا يصل على خيط الواجهة

لكشف التغيير استخدم UISettings أيضاً. يقع حدث ColorValuesChanged عند تغيّر قيم اللون، والدليل الرسمي أيضاً يتتبع تغيّر الإعداد بهذا الحدث.53 هنا تنبيه عملي واحد. لا ضمان لوصول هذا الحدث على خيط الواجهة. ارجع إلى خيط الواجهة بـ Dispatcher في WPF أو Control.Invoke في WinForms أو SynchronizationContext الصالح لكليهما ثمّ المس التحكّمات. معاملة خيط الواجهة ملخّصة في «تنظيم async وخيط الواجهة في WPF/WinForms في صفحة واحدة».

كذلك يقع هذا الحدث عند تغيّر لون التمييز. إن أردت إعادة الطلاء عند تغيّر الفاتح/الداكن فقط فاحكم من جديد عند كلّ حدث ولا تُشعر إلّا عندما يختلف عن السابق. ووفق أولوية الفصل 2، إن كانت سمة التباين مفعّلة فلا تنظر إلى سطوع لون المقدّمة. سمة تباين ذات خلفية داكنة مثل Aquatic مقدّمتها ساطعة، فالنظر إلى السطوع وحده يخطئ الحكم «داكن». احكم سمة التباين حالة مستقلّة أوّلاً.

الصنف التالي يجمع ترتيب الحكم هذا في واحد. مرّر من جهة الاستدعاء SynchronizationContext للرجوع إلى خيط الواجهة، وحكم سمة التباين (في WinForms SystemInformation.HighContrast، وفي WPF SystemParameters.HighContrast، الفصل 8). انتبه إلى توقيت الإنشاء. في WinForms عند Program.Main لا حلقة رسائل بعد ولا Control، وSynchronizationContext.Current هو null. مرّر SynchronizationContext.Current بعد إنشاء التحكّم، كما في منشئ النموذج أو OnLoad. في WPF يمكن تمرير new DispatcherSynchronizationContext(Application.Current.Dispatcher).

using Windows.UI.ViewManagement; // TargetFramework: net8.0-windows10.0.19041.0 فما بعده

public enum ThemeState { Light, Dark, HighContrast }

public sealed class SystemThemeWatcher : IDisposable
{
    private readonly UISettings _settings = new();
    private readonly SynchronizationContext _ui;
    private readonly Func<bool> _isHighContrast;

    private bool _disposed;

    public ThemeState Current { get; private set; }
    public event EventHandler? Changed;

    // ui: SynchronizationContext لخيط الواجهة. مرّر
    //     SynchronizationContext.Current بعد إنشاء التحكّم، أو في WPF DispatcherSynchronizationContext
    // isHighContrast: () => SystemInformation.HighContrast (WinForms)
    //                 () => SystemParameters.HighContrast (WPF)
    public SystemThemeWatcher(SynchronizationContext ui, Func<bool> isHighContrast)
    {
        _ui = ui ?? throw new ArgumentNullException(nameof(ui));
        _isHighContrast = isHighContrast ?? throw new ArgumentNullException(nameof(isHighContrast));
        Current = Read();
        _settings.ColorValuesChanged += OnColorValuesChanged;
    }

    private ThemeState Read()
    {
        // ترتيب الحكم كما في الفصل 2: سمة التباين أوّلاً. سمة تباين ذات خلفية داكنة
        // مقدّمتها ساطعة، فالنظر إلى السطوع وحده يخطئ الحكم «داكن»
        if (_isHighContrast()) return ThemeState.HighContrast;
        // الحكم نفسه في الدليل الرسمي: إن كانت المقدّمة (لون النصّ الافتراضي) ساطعة فداكن
        var fg = _settings.GetColorValue(UIColorType.Foreground);
        bool isDark = (5 * fg.G + 2 * fg.R + fg.B) > 8 * 128;
        return isDark ? ThemeState.Dark : ThemeState.Light;
    }

    // يُستدعى من خيط الواجهة. يمكن استدعاؤه أيضاً من إشعار مسار آخر مثل UserPreferenceChanged
    public void Refresh()
    {
        if (_disposed) return;
        var next = Read();
        // بين الفاتح والداكن لا تُشعر إن كانت الحالة نفسها لتجاهل تغيّر لون التمييز وحده ونحوه.
        // الاستثناء أثناء سمة التباين: تحرير المستخدم لألوان السمة يبقي الحالة HighContrast
        // فأعلم حتّى في الحالة نفسها لإعادة أخذ ألوان النظام
        if (next == Current && next != ThemeState.HighContrast) return;
        Current = next;
        Changed?.Invoke(this, EventArgs.Empty);
    }

    private void OnColorValuesChanged(UISettings sender, object args)
    {
        // لا ضمان للوصول على خيط الواجهة، فارجع إلى الواجهة ثمّ احكم وأعلم.
        // الاستدعاء الذي يصل بعد Dispose (كان في الطابور سلفاً) يُتجاهل بعلم Refresh
        _ui.Post(_ => Refresh(), null);
    }

    public void Dispose()
    {
        // إلغاء الاشتراك يوقف التسليم اللاحق فقط، وتبقى الاستدعاءات المُلقاة سلفاً على خيط الواجهة.
        // ارفع علماً واجعل جهة الاستدعاء المتبقّي تتجاهل (يُستدعى على خيط الواجهة)
        _disposed = true;
        _settings.ColorValuesChanged -= OnColorValuesChanged;
    }
}

على مستوى Win32 يُرسل WM_SETTINGCHANGE إلى كلّ نوافذ المستوى الأعلى عند تغيّر الإعداد،22 وفي .NET يصل ذلك بوصفه SystemEvents.UserPreferenceChanged.23 عند تبديل النمط البصري (بما فيه تفعيل سمة التباين) يأتي WM_THEMECHANGED،24 وعند تغيّر ألوان النظام يأتي WM_SYSCOLORCHANGE.25 أمتن من كتابة معالجة منفصلة لكلّ نوع إشعار أن تستدعي المعالجة نفسها «اقرأ وأعد الطلاء» أيّاً كان الإشعار، وهذا الشكل نفسه لتتبع سمة التباين في المقالة السابقة. في SystemThemeWatcher أعلاه يعني استدعاء Refresh() أيضاً من معالج UserPreferenceChanged أو StaticPropertyChanged.

مسارات إشعار تغيّر السمة والخيطColorValuesChanged في UISettings قد يصل خارج خيط الواجهة فارجع إلى الواجهة، وWM_SETTINGCHANGE يصل بوصفه SystemEvents.UserPreferenceChanged، وWM_THEMECHANGED وWM_SYSCOLORCHANGE يصلان إلى إجراء النافذة. اجمع كلاً في إعادة التطبيق نفسهاColorValuesChangedارجع إلى خيط الواجهةUserPreferenceChangedاقرأ وأعد الطلاءWM_THEMECHANGED ونحوه

الشكل 9: مسارات الإشعار متعدّدة، لكن اجمعها كلّها في المعالجة نفسها «اقرأ وأعد الطلاء».

5.3. أعد الطلاء ── اجمع الألوان في موضع واحد

فرض إمكان إعادة الطلاء أن الألوان مجتمعة في موضع واحد. إن تفرّق Color.White أو #FFFFFF في النموذج وXAML تعذّر تعداد مواضع إعادة الطلاء. في WinForms اصنع صنفاً «لوحة» (نسختان للفاتح والداكن) وخذ التحكّمات الألوان منها عند التشغيل وعند الإشعار. في WPF اجمع الألوان في ResourceDictionary واجعل XAML يشير بـ DynamicResource. في WinUI هذه البنية مُعدَّة من الأصل بوصفها ThemeDictionaries.

بنية جمع الألوان في موضع واحداحمل في التطبيق لوحة للفاتح ولوحة للداكن، ولتأخذ كلّ شاشة الألوان من اللوحة المختارة وفق الوضع الحالي، فيمكن تعداد أهداف إعادة الطلاءفاتحداكنالوضع الحاليأيّهما تُستخدم؟لوحة الفاتحلوحة الداكنكلّ شاشة وكلّ تحكّمColor.White المتفرّقلا يمكن تعداد مواضع إعادة الطلاء

الشكل 10: إن حُملت اللوحة في موضع واحد أمكن تعداد إعادة الطلاء، والتثبيت البرمجي المتفرّق لا يمكن.

عمل «جمع الألوان» هذا ينفع كما هو في دعم سمة التباين وفي تأكيد نسبة التباين الآتي. أكبر تكلفة لدعم الوضع الداكن ليست استدعاء الواجهة بل هذا الترتيب.

6. التنفيذ في WinForms

6.1. .NET 9/10 ── Application.SetColorMode

دخل إلى WinForms دعم أولي للوضع الداكن في .NET 9، و«دُمج كاملاً» في .NET 10. القيم التي تُمرَّر إلى Application.SetColorMode ثلاث.67

  • SystemColorMode.Classic ── الافتراض. فاتح كما كان.
  • SystemColorMode.System ── اتبع إعداد الفاتح/الداكن في Windows.
  • SystemColorMode.Dark ── داكن.

موضع الاستدعاء قبل Application.Run، قبل إنشاء عناصر الواجهة. في .NET 9 كانت ميزة تجريبية فيصير خطأ ترجمة إن لم تُكبت WFO5001 في ملفّ المشروع، ومن .NET 10 لا يظهر هذا الخطأ.26

static class Program
{
    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();
        Application.SetColorMode(SystemColorMode.System); // استدعِ قبل إنشاء الواجهة
        Application.Run(new MainForm());
    }
}

عندما يتغيّر وضع اللون تتبدّل System.Drawing.SystemColors إلى الألوان المقابلة، وتُرسم التحكّمات القياسية وفقها.6 الآلية أن الخاصّية التجريبية SystemColors.UseAlternativeColorSet (SYSLIB5002) «تجعل KnownColor للنظام يعيد مجموعة ألوان بديلة (حالياً نسخة الوضع الداكن)»، وألوان نظام Win32 نفسها لا تتغيّر بإعداد الفاتح/الداكن، فالمجموعة البديلة عند جهة .NET. الوثائق نفسها تقول أيضاً إن ألوان Windows الحالية تُعاد دائماً عندما تكون سمة التباين مفعّلة.27

أمسك ثلاثة قيود مكتوبة في وثائق SetColorMode.2

  1. وضع اللون الداكن متاح على Windows 11 فما بعده فقط.
  2. الوضع الداكن غير متاح عندما تكون سمة التباين مفعّلة.
  3. حتّى مع SystemColorMode.System لا يتبع التطبيق تلقائيّاً تغيّر إعداد Windows أثناء التشغيل.

الثالث موضع استفسار شائع في تطبيقات الأعمال. اكتب في الشرح للمستخدم أنّه يُقرَّر بإعداد Windows عند التشغيل ويسري عند التشغيل التالي. إن أردت حتماً التتبع أثناء التشغيل لزم تصميم إعادة إنشاء النموذج، وقلّما يستحقّ ذلك.

تدفّق SetColorMode وقيودهيُستدعى SetColorMode قبل Application.Run، فتتبدّل SystemColors إلى المجموعة البديلة وتتبع التحكّمات القياسية. القيود الثلاثة Windows 11 فقط، معطّل أثناء سمة تباين، لا يتبع تغيّر الإعداد أثناء التشغيلSetColorMode (System)قبل Application.RunSystemColors إلى المجموعة البديلةتتبع التحكّمات القياسيةWindows 11 فقطمعطّل أثناء سمة تباينلا يتبع التغيّر أثناء التشغيل

الشكل 11: SetColorMode ينفع مرّة واحدة قبل التشغيل، وله ثلاثة قيود موثَّقة.

6.2. التحكّمات المرسومة ذاتيّاً وApplyThemingImplicitly

التحكّمات القياسية تتبع وضع لون التطبيق، لكن وثائق .NET 10 تذكر حالتين استثنائيتين. إن استخدمت داخل تحكّم تكوّنه وترسمه بنفسك تحكّمات Win32 مشتركة مثل أشرطة التمرير، بقيت فاتحة ما لم تشترك صراحة. وبالعكس، إن ورثت تحكّماً قائماً يتبع السمة وأردت التحكّم الكامل في الرسم فاخرج من الاشتراك.7

في الحالتين تجاوز Control.CreateParams واستدعِ SetStyle(ControlStyles.ApplyThemingImplicitly, true/false) قبل قراءة base.CreateParams. فخ هذه الواجهة أن منشئ الصنف الأساس يقرأ CreateParams، فمنشئك أنت متأخّر.7

public partial class GanttChartControl : Control
{
    protected override CreateParams CreateParams
    {
        get
        {
            // اضبط قبل قراءة base.CreateParams. في المنشئ متأخّر
            SetStyle(ControlStyles.ApplyThemingImplicitly, true);
            return base.CreateParams;
        }
    }
}
توقيت يمكن فيه ضبط ApplyThemingImplicitlyيُقرَّر ApplyThemingImplicitly عندما يقرأ منشئ الصنف الأساس CreateParams، فيلزم استدعاء SetStyle داخل تجاوز CreateParams قبل base.CreateParams، ومنشئ الصنف المشتق متأخّرمنشئ الأساسيقرأ CreateParamsيلزم SetStyle قبل هنامنشئ الصنف المشتقالاستدعاء هنا متأخّر

الشكل 12: يجب أن يكون ApplyThemingImplicitly مقرَّراً قبل أن يقرأ منشئ الأساس CreateParams.

الرسم الذاتي نفسه (OnPaint برسم GDI+) يتبع المجموعة البديلة إن استخدمت SystemColors / SystemBrushes / SystemPens. المواضع المطلية بـ Color.White تُستبدل هنا أيضاً باللوحة السابقة.

6.3. .NET Framework 4.8 و.NET 8 فما قبله ── صرّح بـ «تثبيت الفاتح»

في بيئة بلا SetColorMode لا دعم قياسي للوضع الداكن. الخياران اثنان.

  1. صرّح بتثبيت الفاتح. أبقِ سمة DWM على FALSE الافتراضي (شريط عنوان فاتح دائماً)، ومنطقة العميل كما كانت. دعم سمة التباين (الفصل 8) وحده لازم حتماً.
  2. ادعم جملة بنفسك. اجمع اللوحة، واقرأ بـ UISettings واتبع، واجعل سمة DWM TRUE، وأعد طلاء مظهر التحكّمات المشتركة أيضاً كلّه.

الثاني يميل إلى حالة «داكن تقريباً لكن فاتح في مواضع» لأن التطبيق لا يتحكّم كاملاً في رسم تحكّمات Win32 المشتركة (أشرطة التمرير والترويسة وأزرار توسيع الشجرة وغيرها). كما يقول الدليل الرسمي «يلزم أن يتبع السطح كلّه»،3 الداكن الناقص تجربة أسوأ من تثبيت الفاتح. للأصل القائم اختيار 1 والتصريح بأن «هذا التطبيق عرض فاتح»، والتبديل إلى SetColorMode عند الترحيل إلى .NET 10 سياسة واقعية وأسهل شرحاً.

خيارات WinForms حسب وقت التشغيلعلى .NET 10 فما بعده استخدم SetColorMode، وعلى .NET 9 استخدم الواجهة نفسها مع كبت WFO5001، وعلى .NET 8 فما قبله و.NET Framework اختر التصريح بتثبيت الفاتح أو الدعم الكامل الذاتي بما فيه التحكّمات المشتركة.NET 10 فما بعده.NET 9.NET 8 فما قبله / .NET Frameworkموصى بهإن كنت مستعدّاًوقت التشغيل هو؟SetColorMode (System)SetColorMode + كبت WFO5001ماذا تفعل؟صرّح بتثبيت الفاتحادعم جملة بنفسكتبقى التحكّمات المشتركة

الشكل 13: على أوقات تشغيل بلا SetColorMode يصير التصريح بتثبيت الفاتح الافتراض الواقعي.

7. التنفيذ في WPF

7.1. .NET 9/10 ── سمة Fluent وThemeMode

تضمّ WPF في .NET 9 سمة جديدة وفق تصميم Fluent في Windows 11، وتدعم الفاتح/الداكن ولون التمييز. طريقان للتطبيق: ضبط خاصّية ThemeMode، أو إضافة قاموس موارد PresentationFramework.Fluent إلى MergedDictionaries.8

قيم ThemeMode أربع: Light / Dark / System / None (الافتراض. سمة Aero2 التقليدية)، والضبط على Application ينفع التطبيق كلّه وعلى Window تلك النافذة فقط.8

<Application x:Class="OrderEntry.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml"
             ThemeMode="System">
</Application>

ThemeMode لا يحمّل قاموس سمة Fluent إلى الموارد فقط، بل منصوص أنّه «يتحكّم أيضاً في تطبيق مادّة الخلفية والوضع الداكن على النافذة». أي أن سمة DWM في الفصل 4 تتولاها جهة WPF. كذلك صُمِّم ThemeMode وResources ليعملان متزامنين، ويُشرح ذلك لتفادي عدم اتساق نافذة داكنة وتحكّمات داخلها فاتحة.10

ما يفعله ThemeMode في WPFجعل ThemeMode هو System يحمّل قاموس سمة Fluent الموافق لإعداد Windows إلى الموارد، ويتحكّم معه في تطبيق الوضع الداكن ومادّة الخلفية على النافذةThemeMode=Systemاقرأ إعداد Windowsحمّل قاموس Fluent إلى المواردتغميق النافذة ومادّة الخلفيةتزامن مع Resources لتفادي عدم الاتساق

الشكل 14: يتحكّم ThemeMode معاً في تحميل قاموس Fluent وتغميق النافذة.

تنبيهان ينبغي معرفتها قبل الاعتماد. الأوّل أن قراءة ThemeMode وكتابته من الشيفرة ميزة تجريبية، والوصول يصير خطأ WPF0001. إن كبتّ أمكنت كتابة مثل Application.Current.ThemeMode = ThemeMode.Dark، لكن مرجع الواجهة يبقى [Experimental("WPF0001")] حتّى في .NET 10 ويلاحظ أنّه «قد يُحذف لاحقاً».810 الثاني أن دعم نمط Fluent «قيد العمل» حتّى في .NET 10. في .NET 10 أُضيفت أنماط DatePicker وGridSplitter وGroupBox وTextBox وغيرها، وأُصلح انهيار مرتبط بـ HighContrast،9 أي أن Fluent في .NET 9 كان يفتقدها. قبل حكم الاعتماد أكّد على جهاز حقيقي أن التحكّمات المستخدمة في تطبيق الأعمال (خصوصاً DataGrid ومنتجات أطراف ثالثة) لا تنهار تحت Fluent.

7.2. التتبع مع بقاء السمة التقليدية ── تبديل ResourceDictionary

في WPF التي لا تعتمد Fluent (أو على .NET 8 فما قبله و.NET Framework) لا دعم قياسي للوضع الداكن. ألوان نظام Win32 لا تتغيّر بإعداد الفاتح/الداكن، فالإشارة إلى SystemColors في WPF لا تُعتم. بنية التتبع الذاتي كما يلي.

  1. عرّف الألوان والفرش بالمفاتيح نفسها في Themes/Light.xaml للفاتح وThemes/Dark.xaml للداكن.
  2. من XAML أشر بـ DynamicResource مثل {DynamicResource App.WindowBackgroundBrush} (StaticResource يُثبَّت عند التحميل فلا يتبع التبديل).
  3. عند إشعار SystemThemeWatcher في الفصل 5 بدّل القاموس المقابل في MergedDictionaries. أثناء سمة التباين أيّ قاموس وضعت يُستبدل بألوان النظام بمحفّز 8.4، فأبقِ قاموس الفاتح.
public static class AppTheme
{
    private static readonly Uri Light = new("pack://application:,,,/Themes/Light.xaml");
    private static readonly Uri Dark = new("pack://application:,,,/Themes/Dark.xaml");

    public static void Apply(ThemeState state)
    {
        var merged = Application.Current.Resources.MergedDictionaries;
        var current = merged.FirstOrDefault(d => d.Source == Light || d.Source == Dark);
        // قاموس الداكن في الداكن فقط. أثناء سمة التباين أوكل إلى ألوان النظام (8.4)
        var next = new ResourceDictionary { Source = state == ThemeState.Dark ? Dark : Light };
        if (current is null)
        {
            merged.Add(next);
        }
        else
        {
            merged[merged.IndexOf(current)] = next; // بدّل في الموضع نفسه
        }
    }
}
تبديل قاموس الموارد في WPFعرّف الألوان بالمفاتيح نفسها في قاموس الفاتح وقاموس الداكن، واجعل XAML يشير بـ DynamicResource، وبدّل القاموس داخل MergedDictionaries عند إشعار تغيّر السمة فيُحدَّث موضع الإشارةإشعار تغيّر السمةبدّل القاموس في MergedDictionariesLight.xaml (المفاتيح نفسها)Dark.xaml (المفاتيح نفسها)تُحدَّث إشارة DynamicResourceإشارة StaticResourceتبقى قيمة وقت التحميل

الشكل 15: إن بدّلت قاموساً بالمفاتيح نفسها تبعت إشارة DynamicResource وحدها.

قوالب التحكّمات القياسية (خلفية الزرّ ولون شريط التمرير) تحمل ألوان السمة التقليدية، فتبقى هنا أيضاً مواضع «سطح التطبيق داكن والتحكّم القياسي فاتح». قدّر حجم العمل لتجاوز النمط لكلّ تحكّم يلزم، ثمّ قارن باعتماد Fluent أو تثبيت الفاتح.

7.3. شريط العنوان

إن استخدمت ThemeMode تولّته WPF. إن تتبّعت بنفسك مع بقاء السمة التقليدية فاستخدم ضبط سمة DWM من OnSourceInitialized في 4.2، وأعد الضبط أيضاً عند إشعار SystemThemeWatcher.

8. الرسم تحت سمة التباين ── احفظ أزواج ألوان النظام

8.1. الكشف والإشعار

في Win32 مرّر SPI_GETHIGHCONTRAST إلى SystemParametersInfo واستقبل بنية HIGHCONTRAST، واحكم ببتّ HCF_HIGHCONTRASTON في dwFlags. يلزم ضبط cbSize عند الاستدعاء.1128 تصنّف Microsoft هذا «الطريقة المدعومة الوحيدة لتأكيد وجود التباين العالي».12

bool IsContrastThemeActive()
{
    HIGHCONTRASTW hc{};
    hc.cbSize = sizeof(hc);
    if (!::SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(hc), &hc, 0))
    {
        // لا تغطّ الفشل بقيمة افتراضية. أظهره برمز خطأ يتبيّن منه السبب
        throw std::system_error(::GetLastError(), std::system_category(),
                                "SystemParametersInfo(SPI_GETHIGHCONTRAST)");
    }
    return (hc.dwFlags & HCF_HIGHCONTRASTON) != 0;
}

لكلّ إطار خاصّية تغلف هذا الاستدعاء.

البيئة الحكم إشعار التغيير
Win32 / MFC SPI_GETHIGHCONTRAST + HCF_HIGHCONTRASTON WM_SYSCOLORCHANGE، WM_THEMECHANGED
WinForms SystemInformation.HighContrast SystemEvents.UserPreferenceChanged
WPF SystemParameters.HighContrast (يقابل SPI_GETHIGHCONTRAST) SystemParameters.StaticPropertyChanged
WinUI 3 ThemeSettings.HighContrast (Microsoft.UI.System) ThemeSettings.Changed

دليل إمكانية الوصول في WinForms يطلب تأكيد HighContrast عند التشغيل والاستجابة للتغيّر بـ UserPreferenceChanged.13 SystemParameters.HighContrast في WPF مقابل لـ SPI_GETHIGHCONTRAST وHCF_HIGHCONTRASTON،29 وتغيّر الخاصّية الثابتة يُشعَر بـ StaticPropertyChanged.30 ThemeSettings في WinUI 3 يُنشأ مربوطاً بالنافذة بـ CreateForWindowId ويُشترَك في حدث Changed، وانتبه إلى أن الحدث يتوقّف إن لم تُبقِ مرجعاً إلى الكائن.31

مسار كشف سمة التباينSPI_GETHIGHCONTRAST في Win32 طريقة الحكم المدعومة الوحيدة، وSystemInformation.HighContrast في WinForms وSystemParameters.HighContrast في WPF وThemeSettings.HighContrast في WinUI 3 تُقدَّم مغلفة لهذا من كلّ إطارSPI_GETHIGHCONTRAST (طريقة الحكم الوحيدة)WinForms SystemInformationWPF SystemParametersWinUI 3 ThemeSettingsWin32 استدعاء مباشر

الشكل 16: جذر الحكم واجهة Win32 واحدة، ولكلّ إطار خاصّية تغلفها.

8.2. مبدأ الرسم ── زوج المقدّمة والخلفية

تورد «High contrast parameter» من Microsoft ثلاثة أمور ينبغي للتطبيق فعلها عندما يكون التباين العالي مفعّلاً.11

  1. انقل كلّ الألوان إلى مجموعة واحدة من لون مقدّمة ولون خلفية. استخدم بـ GetSysColor زوج COLOR_WINDOWTEXT وCOLOR_WINDOW، أو زوج COLOR_BTNTEXT وCOLOR_BTNFACE.
  2. احذف صور النقطية المعروضة خلف النصّ. عائق بصري لمن يحتاجون التباين العالي.
  3. ارسم الصور متعدّدة الألوان بلون مقدّمة النصّ ولون الخلفية.

«الزوج» هو الجوهر. دليل Windows 8 فما بعده يشرح أن COLOR_HIGHLIGHTTEXT صُنع على فرض خلفية COLOR_HIGHLIGHT، وCOLOR_WINDOWTEXT على فرض خلفية COLOR_WINDOW، ويطلب عدم تثبيت لون النصّ برمجيّاً وجعل الواجهة لا تعتمد على السمة المطبَّقة لأن المستخدم يخصّص الألوان.12 مثال الدليل نفسه «في Aero النصّ دائماً أسود ولون التحديد أزرق فاتح، لكن في High Contrast Black يصير لون التحديد أسود. إن افترضت نصّاً أسود واستخدمت لون تحديد النظام صار نصّاً أسود على أرض سوداء» هو بعينه «اختفى عرض الحالة» في المقدّمة.

دليل سمات التباين في Windows 11 يجعل هذا التقابل جدولاً.1

الاستخدام المقدّمة الخلفية
العناوين والنصّ والقوائم والإطارات وواجهة لا تُشغَّل SystemColorWindowText SystemColorWindow
ارتباط تشعّبي SystemColorHotlight SystemColorWindow
واجهة معطّلة أو غير نشطة SystemColorGrayText SystemColorWindow
التحديد والتحويم والضغط والجاري SystemColorHighlightText SystemColorHighlight
واجهة تُشغَّل مثل الأزرار SystemColorButtonText SystemColorButtonFace

و«ما لا ينبغي فعله» منصوص أيضاً. لا تستخدم GrayText لنصّ تكميلي أو تلميح (مخصّص لحالة التعطيل)، لا تستخدم Hotlight لغير الارتباط التشعّبي، لا تخلط مقدّمة وخلفية غير متوافقتين، لا تختر اللون بالمظهر وحده (المستخدم يغيّر الألوان فعليّاً). اجعل خلفية الصفحة واللوح والنافذة المنبثقة معيارها SystemColorWindow، وخلفيات الأوجه المتجاورة تصير اللون نفسه فافصل الحدود اللازمة فقط بإطار مخصّص لسمة التباين (يُوصى بـ 2px للقوائم المنبثقة والحوارات)، وهذا أيضاً توجيه تصميم.1

تدفّق تعذّر القراءة عند كسر الزوجإن افترضت أن النصّ أسود واستخدمت لون تحديد النظام للخلفية وحدها صار لون التحديد أسود في High Contrast Black فنصّ أسود على أرض سوداء. إن أخذت المقدّمة والخلفية زوجاً بقيت القراءة ممكنة ولو حرّر المستخدم الألوانافترض أن النصّ أسودخلفية التحديد وحدها لون تحديد النظامفي High Contrast Black لون التحديد أسودنصّ أسود على أرض سوداءخذ المقدّمة والخلفية زوجاًتبقى القراءة ممكنة ولو حرّر المستخدم الألوانيحرّر المستخدم الألوان

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

حكم الرسم تحت سمة التباينإن كانت سمة التباين مفعّلة فانقل الألوان إلى أزواج ألوان النظام، واحذف الصور خلف النصّ، وارسم الرسوم متعدّدة الألوان بلوني المقدّمة والخلفية، ولا تستخدم ألواناً مثبَّتة برمجيّاًسمة التباين مفعّلةانقل الألوان إلى أزواجاحذف الصور خلف النصّارسم الرسوم متعدّدة الألوان بلونينلا تستخدم ألواناً مثبَّتة برمجيّاً

الشكل 18: الرسم عندما تكون سمة التباين مفعّلة يُلخَّص في النقل والحذف والتحويل إلى لونين وترك التثبيت البرمجي.

8.3. التنفيذ في WinForms

التحكّمات القياسية في WinForms تتبع ألوان النظام إن تركت ForeColor / BackColor على الافتراض. ما تبدّله وفق الحكم هو مواضع اللون الخاصّ والرسم الذاتي فقط. مثال الدليل لصاقة صفراء على أرض زرقاء في الحالة العادية، وتعود تحت التباين العالي إلى SystemColors.Window / SystemColors.WindowText.13 شكل يضيف تفرّع سمة التباين إلى بنية اللوحة السابقة.

using Microsoft.Win32;

public partial class OrderForm : Form
{
    private readonly SynchronizationContext _ui;

    public OrderForm()
    {
        InitializeComponent();
        // بعد إنشاء التحكّم فـ WindowsFormsSynchronizationContext موجود
        _ui = SynchronizationContext.Current
              ?? throw new InvalidOperationException("أنشئ على خيط الواجهة.");
        ApplyColorScheme();
        SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged;
    }

    private void ApplyColorScheme()
    {
        if (SystemInformation.HighContrast)
        {
            // أوكل جملة إلى ألوان النظام مع حفظ الزوج، وأزل الصورة خلف النصّ
            statusLabel.BackColor = SystemColors.Window;
            statusLabel.ForeColor = SystemColors.WindowText;
            headerPanel.BackgroundImage = null;
        }
        else
        {
            var p = AppPalette.Current; // لوحة الفاتح/الداكن (الفصل 5)
            statusLabel.BackColor = p.PanelBackground;
            statusLabel.ForeColor = p.PanelForeground;
            headerPanel.BackgroundImage = Properties.Resources.HeaderPattern;
        }
    }

    private void OnUserPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e)
    {
        // لا ضمان لوصول هذا الحدث أيضاً على خيط الواجهة. ارجع إلى الواجهة ثمّ احكم من جديد بلا تضييق بالفئة
        _ui.Post(_ =>
        {
            if (IsDisposed) return;
            ApplyColorScheme();
        }, null);
    }

    // حدث ثابت فيتسرب النموذج إن لم تُزل. أزل في Dispose(bool) الذي يمرّ أيضاً بمسار يُتلف بلا إغلاق
    // (إن وُجد Dispose(bool) مولَّد من المصمّم فاكتب هناك)
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged;
        }
        base.Dispose(disposing);
    }
}

الرسم الذاتي في OnPaint ارسمه بفرش نظام تحفظ الزوج مثل SystemBrushes.Window / SystemPens.WindowText، واستبدل رسماً متعدّد الألوان مثل دائرة ملوّنة تمثّل الحالة بإطار بلون المقدّمة ونصّ («يعمل» / «متوقّف»). العرض الذي لا يعتمد على اللون وحده هو حديث معيار النجاح 1.4.1 في المقالة السابقة.

تفرّع تطبيق الألوان في WinFormsاستدعِ ApplyColorScheme عند التشغيل وعند كلّ UserPreferenceChanged، فإن كان SystemInformation.HighContrast صادقاً فأوكل إلى أزواج ألوان النظام وأزل صورة الخلفية، وإن كان كاذباً فخذ الألوان من لوحة الفاتح/الداكنصادقكاذبعند التشغيل / UserPreferenceChangedApplyColorSchemeSystemInformation.HighContrast؟أوكل إلى أزواج SystemColorsأزل صورة الخلفيةخذ من لوحة الفاتح/الداكن

الشكل 19: في WinForms استدعِ المعالجة نفسها عند التشغيل وعند كلّ إشعار، وأوكل إلى ألوان النظام إن كانت سمة التباين.

8.4. التنفيذ في WPF

SystemColors في WPF إن أشرت إلى مفتاح موارد مثل WindowBrushKey بـ DynamicResource تُحدَّث تلقائيّاً عند تغيّر الفرشاة (الإشارة الثابتة التي تستخدم WindowBrush مباشرة لا تُحدَّث).32 لتبديل المظهر أثناء سمة التباين فقط أشر إلى قيمة SystemParameters.HighContrast من DataTrigger. لكن SystemParameters.HighContrast خاصّية ثابتة فلا تصير كما هي مصدراً حيّاً للربط. جهّز وكيلاً صغيراً واحداً يشترك في StaticPropertyChanged ويحفظ القيمة ويُشعر بـ INotifyPropertyChanged، واربط محدّداً ذلك المثيل في Source.30

public sealed class ThemeSettings : INotifyPropertyChanged
{
    public static ThemeSettings Instance { get; } = new();

    public bool IsHighContrast { get; private set; } = SystemParameters.HighContrast;
    public event PropertyChangedEventHandler? PropertyChanged;

    private ThemeSettings()
    {
        // يُشعَر عندما تتغيّر الخاصّية الثابتة في SystemParameters (يُعاد أخذ SPI_GETHIGHCONTRAST)
        SystemParameters.StaticPropertyChanged += (_, e) =>
        {
            if (!string.IsNullOrEmpty(e.PropertyName)
                && e.PropertyName != nameof(SystemParameters.HighContrast)) return;
            IsHighContrast = SystemParameters.HighContrast;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsHighContrast)));
        };
    }
}
<!-- أعلن xmlns:local="clr-namespace:OrderEntry" سلفاً -->
<Style x:Key="CardStyle" TargetType="Border">
    <Setter Property="Background" Value="{DynamicResource App.CardBackgroundBrush}"/>
    <Setter Property="BorderBrush" Value="{DynamicResource App.CardBorderBrush}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Style.Triggers>
        <DataTrigger Binding="{Binding Source={x:Static local:ThemeSettings.Instance}, Path=IsHighContrast}"
                     Value="True">
            <!-- احفظ الزوج: الخلفية Window، والإطار والنصّ WindowText. اجعل الحدّ أثخن -->
            <Setter Property="Background"
                    Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
            <Setter Property="BorderBrush"
                    Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"/>
            <!-- ورّث أيضاً إلى النصّ الداخلي. العناصر الابن التي تصرّح بـ Foreground تقطع التوريث فانتبه -->
            <Setter Property="TextElement.Foreground"
                    Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"/>
            <Setter Property="BorderThickness" Value="2"/>
        </DataTrigger>
    </Style.Triggers>
</Style>
إشارة ألوان النظام في WPF ومحفّز سمة التباينالإشارة إلى مفتاح موارد SystemColors بـ DynamicResource تتبع تغيّر الفرشاة تلقائيّاً، والمحفّز المربوط بـ IsHighContrast في وكيل يشترك في StaticPropertyChanged يستجيب للتبديل أثناء التشغيل ويبدّل إلى ألوان تحفظ الزوج. الإشارة المباشرة إلى WindowBrush لا تُحدَّثأشر إلى WindowBrushKey بـ DynamicResourceتتبع تلقائي لتغيّر الفرشاةStaticPropertyChangedIsHighContrast في الوكيليستجيب DataTriggerبدّل إلى ألوان تحفظ الزوجأشر إلى WindowBrush مباشرةلا تُحدَّث

الشكل 20: تتبع WPF التبديل أثناء التشغيل بإشارة ديناميكية إلى مفتاح الموارد وربط بوكيل ينقل تغيّر الخاصّية الثابتة.

حتّى عند استخدام سمة Fluent، اجعل تأكيد التشغيل تحت سمة التباين شرط اعتماد، على ضوء إصلاح الانهيار المرتبط بـ HighContrast في .NET 10.9

8.5. التنفيذ في WinUI 3

في WinUI 3 تتبع التحكّمات القياسية الفاتح/الداكن وسمة التباين من الأصل، وألوان التطبيق الخاصّة تُعرَّف في ResourceDictionary.ThemeDictionaries بمفاتيح Default (داكن) وLight وHighContrast. في HighContrast لا تثبّت الألوان برمجيّاً، وأشر إلى ألوان نظام ديناميكية مثل SystemColorWindowColor بـ ThemeResource. للتحكّم المخصّص الذي يحمل Light/Dark جهّز دائماً HighContrast أيضاً، وHighContrast مفتاح احتياطي عندما لا توجد سمة تباين عالٍ مسمّاة أخرى.331

<ResourceDictionary.ThemeDictionaries>
    <ResourceDictionary x:Key="Default">
        <SolidColorBrush x:Key="App.CardBackgroundBrush" Color="#2B2B2B"/>
    </ResourceDictionary>
    <ResourceDictionary x:Key="Light">
        <SolidColorBrush x:Key="App.CardBackgroundBrush" Color="#F3F3F3"/>
    </ResourceDictionary>
    <ResourceDictionary x:Key="HighContrast">
        <SolidColorBrush x:Key="App.CardBackgroundBrush"
                         Color="{ThemeResource SystemColorWindowColor}"/>
    </ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>

وآلية أخرى في WinUI اسمها HighContrastAdjustment، مفعّلة افتراضيّاً. تفرض نصّاً أبيض وخلفية تمييز سوداء للحفاظ على التباين، وإن جهّزت قاموس سمة يستخدم ألوان النظام استخداماً صحيحاً فاضبطها على None وأمرّر نمطك، وهذا توصية الدليل.1

حلّ ThemeDictionaries في WinUIيُختار وفق السمة الحالية أحد القواميس Default (داكن) أو Light أو HighContrast، وفي HighContrast أشر إلى ألوان نظام ديناميكية بـ ThemeResource. HighContrast مفتاح احتياطي عندما لا توجد سمة تباين عالٍ مسمّاةداكنفاتحسمة تباينالسمة الحالية هي؟قاموس Defaultقاموس Lightقاموس HighContrastأشر إلى SystemColor بـ ThemeResourceاحتياطي عندما لا توجد سمة مسمّاة

الشكل 21: في WinUI يُختار قاموس كلّ سمة تلقائيّاً، وقاموس HighContrast يشير إلى ألوان النظام.

9. دعم الوضع الداكن ليس بديلاً عن دعم إمكانية الوصول

الإبلاغ بأن دعم الوضع الداكن «دعم لإمكانية الوصول» خطأ. العلاقة بينهما تُرتَّب كما يلي.

  • معيار نسبة التباين ينطبق بالمثل على لوحة الداكن. معيار النجاح 1.4.3 في WCAG يطلب 4.5:1 للنصّ و3:1 للنصّ الكبير، ولا يتغيّر هذا إن كانت الخلفية داكنة.14 تصميم داكن يضع نصّاً رماديّاً وسطاً على خلفية رماديّة داكنة يحمل المشكلة نفسها لـ «رمادي فاتح على أرض بيضاء» في الفاتح.
  • تجنّب الأسود الخالص والأبيض الخالص تصميم Windows 11. أفضل ممارسات Microsoft تشرح أن Windows 11 تجنّب الأبيض الخالص والأسود الخالص واعتمد درجات ألطف للعين.34 وبالعكس، جعل الخلفية #000000 في الوضع الداكن يشتكي منه من يجدون تبايناً أشدّ من اللازم مع النصّ الساطع (هالة).
  • مراعاة تنوّع إدراك اللون لازمة بمعزل عن السمة. دليل الألوان من Microsoft يطلب ألّا تجعل اللون وسيلة الإبلاغ الرئيسة بل تعزيزاً بصريّاً، وألّا تجعل توليفة الأحمر والأخضر التمييز الوحيد.3515
  • سمة التباين متطلّب مستقل عن الوضع الداكن. كما في الفصل 2 لا يتاح الوضع الداكن أثناء تفعيل سمة تباين، فمهما كمل دعم الداكن لا يصل إلى مستخدمي سمة التباين.
علاقة الوضع الداكن بإمكانية الوصولدعم الوضع الداكن مراعاة للذوق والبيئة، ومتطلّبات إمكانية الوصول من نسبة التباين والإبلاغ الذي لا يعتمد على اللون وحده ودعم سمة التباين يلزم استيفاؤها على حدة بمعزل عن السمةليس بديلاًدعم الوضع الداكنمراعاة الذوق والبيئةإمكانية الوصولنسبة تباين 4.5:1لا تعتمد على اللون وحدهدعم سمة التباين

الشكل 22: دعم الوضع الداكن مراعاة للذوق والبيئة، ومتطلّبات إمكانية الوصول تُستوفى على حدة.

من جهة أخرى عمل «جمع الألوان في موضع واحد» في الفصل 5 أساس لكليهما. إن كانت اللوحة في موضع واحد أمكن تعداد أهداف قياس نسبة التباين في الفاتح والداكن كلّاً، وكتابة تفرّع سمة التباين في الموضع نفسه. جمع اللوحة بمناسبة دعم الداكن، وتفقّد نسبة التباين وسمة التباين في الأثناء أكثر ترتيب كفاءة استثماراً.

10. اختيار السياسة ── التوصية حسب نوع التطبيق

نوع التطبيق السياسة الموصى بها
WinForms جديد (.NET 10) استخدم SetColorMode(System). اجعل الرسم الذاتي قائماً على SystemColors، والتحكّم الخاصّ الذي يشمل تحكّمات مشتركة اشترك بـ ApplyThemingImplicitly
WPF جديد (.NET 9/10) اعتمد ThemeMode="System" بعد تأكيد عرض التحكّمات المستخدمة وتأكيد التشغيل تحت سمة التباين. إن صعب فالسمة التقليدية + تبديل القاموس
WinForms/WPF قائم (.NET Framework 4.8، .NET 8 فما قبله) صرّح بـ «تثبيت الفاتح» وأبقِ سمة DWM على الافتراض (FALSE). دعم سمة التباين لازم حتماً، وإلى دعم الداكن عند الترحيل إلى .NET 10
WinUI 3 يتبع النظام افتراضيّاً. عرّف الألوان الخاصّة في ThemeDictionaries شاملاً HighContrast، واجعل HighContrastAdjustment هو None
Win32 / MFC سمة DWM + لوحة ذاتية + إعادة الحساب عند WM_THEMECHANGED / WM_SYSCOLORCHANGE. الدليل الرسمي يعالج الكشف وشريط العنوان حتّى، وإعادة طلاء التحكّمات المشتركة خارج النطاق
مسار الأصل القائم إلى دعم الداكنالأصل القائم يصرّح أوّلاً بتثبيت الفاتح، وينهي حتماً دعم سمة التباين، ويجمع اللوحة استعداداً، ثمّ يرحّل إلى .NET 10 ويبدّل إلى SetColorMode أو ThemeMode. دعم الداكن الناقص تجربة أسوأ من تثبيت الفاتح فلا يُمرّ بهلا يُمرّ بهصرّح بتثبيت الفاتح (الآن)دعم سمة التباين (لازم)جمع اللوحة (استعداد)رحّل إلى .NET 10بدّل إلى SetColorMode / ThemeModeدعم داكن ناقصتجربة أسوأ من تثبيت الفاتح

الشكل 23: الأصل القائم يبدأ من تثبيت الفاتح، ويمضي عبر دعم سمة التباين وجمع اللوحة إلى دعم الداكن عند الترحيل إلى .NET 10.

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

11. قائمة تحقّق

بعد انتهاء الدعم أكّد على جهاز حقيقي بهذا الترتيب. يمكن تبديل الكلّ من شاشة الإعدادات في عشرات الثواني.

  1. بدّل الفاتح/الداكن أثناء التشغيل. غيّر الوضع في «الإعدادات > التخصيص > الألوان»، وأكّد هل يتبع شريط العنوان ومنطقة العميل كلاهما، أو أنّه وفق المواصفة «يسري عند التشغيل التالي» (SetColorMode في WinForms لا يتبع).
  2. أحدث إعادة إنشاء المقبض. في WinForms بدّل ShowInTaskbar أثناء التشغيل وأكّد بقاء سمة شريط العنوان.
  3. جرّب سمات التباين الأربع كلّها. بدّل بـ Left Alt+Left Shift+PrintScreen، وأكّد في Aquatic وDesert وDusk وNight sky كلّاً أن النصّ والإطار وصفّ التحديد والعنصر المعطّل والرابط قابلة للقراءة.1
  4. حرّر ألوان سمة التباين. المستخدم يغيّر الألوان فعليّاً. حرّر الخلفية إلى لون متطرّف واكشف ما بقي من تثبيت برمجي.
  5. انظر إلى السجلّ. أكّد أن الفشل يُسجَّل عندما يفشل DwmSetWindowAttribute أو SystemParametersInfo على نظام تشغيل مدعوم، وأن Windows 10 دون الإصدار 22000 يبدأ فاتحاً دون استدعاء سمة DWM.
  6. قس نسبة التباين. في الفاتح والداكن كلّاً تفقّد توليفات لون النصّ ولون الخلفية في اللوحة بـ 4.5:1.14
إجراء تحقّق دعم السمةأكّد على جهاز حقيقي بهذا الترتيب: تبديل الفاتح/الداكن أثناء التشغيل، وإعادة إنشاء المقبض، وسمات التباين الأربع، وتحرير ألوان السمة، وتأكيد سجلّ الفشل، وقياس نسبة التباينبدّل الفاتح/الداكن أثناء التشغيلإعادة إنشاء المقبضسمات التباين الأربعحرّر ألوان السمةتأكيد سجلّ الفشلقياس نسبة التباين

الشكل 24: يبدأ التحقّق من تبديل الإعداد ويُختَم بتأكيد السجلّ ونسبة التباين.

12. الخلاصة

  • سمة Windows محوران «فاتح/داكن» و«سمة تباين»، وما دامت الثانية مفعّلة لا يتاح الوضع الداكن. الحكم سمة التباين أوّلاً.
  • شريط عنوان التطبيق القائم أبيض افتراض للتوافق، وتمرير TRUE لـ DWMWA_USE_IMMERSIVE_DARK_MODE في DwmSetWindowAttribute (القيمة 20، Windows 11 الإصدار 22000 فما بعده) يرسمه داكناً عندما يكون النظام داكناً. اضبط عند كلّ إنشاء HWND، وأبقِ الفشل في السجلّ.
  • الوضع الحالي يُحكم بسطوع لون المقدّمة من UISettings.GetColorValue، ويُلاحَظ التغيير بـ ColorValuesChanged، وترجع إلى خيط الواجهة وتعيد الطلاء. اجمع الألوان في موضع واحد.
  • في WinForms Application.SetColorMode(SystemColorMode.System) في .NET 9/10. أمسك القيود الثلاثة: Windows 11 فقط، معطّل أثناء سمة تباين، لا يتبع التغيّر أثناء التشغيل، وApplyThemingImplicitly للتحكّم الخاصّ.
  • في WPF ThemeMode="System" في .NET 9/10. التشغيل من الشيفرة تجريبي وFluent قيد العمل فاعتمد بعد تقييم. في السمة التقليدية تبديل القاموس + DynamicResource.
  • تحت سمة التباين احكم بسلسلة SPI_GETHIGHCONTRAST، وانقل الألوان إلى أزواج ألوان النظام، واحذف الصور خلف النصّ، وارسم الرسوم متعدّدة الألوان بلونين. GrayText لحالة التعطيل، وHotlight للروابط فقط.
  • دعم الداكن ليس بديلاً عن دعم إمكانية الوصول. في الداكن أيضاً 4.5:1، ولا تعتمد على اللون وحده، ودعم سمة التباين لازم على حدة.
  • للأصل القائم التصريح بـ «تثبيت الفاتح» حلّ واقعي، ودعم سمة التباين وحده لا يمكن تثبيته.

الخطوة الأولى الموصى بها اختيار شاشة رئيسة واحدة، وتفعيل سمة التباين أوّلاً بـ Left Alt+Left Shift+PrintScreen والتأمّل، ثمّ العودة بالمفتاح نفسه، ثمّ تبديل إعداد ألوان Windows إلى الداكن (ما دامت سمة التباين مفعّلة لا يتاح الوضع الداكن فجرّبهما منفصلين). في دقائق يتبيّن «أين يحمل تطبيقكم الألوان».

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

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

تتناول شركة كومورا سوفت ذ.م.م. دعم الوضع الداكن في تطبيقات أعمال WinForms/WPF (جمع اللوحة، وتقييم الترحيل إلى SetColorMode / ThemeMode في .NET 9/10، ودمج سمة DWM)، وتشخيص انهيار العرض تحت سمة التباين وإصلاحه، واستشارة تتبع السمة في أصل Win32/MFC. يمكن البدء من مرحلة «فعّلنا الوضع الداكن فجاءت شكاوى الموظّفين».

روابط مرجعيّة

  1. Microsoft Learn, Contrast themes. عن أن سمة التباين تستخدم لوحة مقيَّدة تقريباً 7:1 فما فوق ولا ينبغي خلطها بسمات الفاتح/الداكن، والأنواع الأربعة Aquatic وDesert وDusk وNight sky وتحرير الألوان، والتبديل بـ Left Alt+Left Shift+PrintScreen، وأزواج المقدّمة/الخلفية لموارد SystemColor والاستخدامات، وجعل GrayText لحالة التعطيل فقط وHotlight للروابط فقط، والانهيار بألوان مثبَّتة برمجيّاً، وإطار الحدود، وHighContrast في ThemeDictionaries، وجعل HighContrastAdjustment هو None، والكشف بـ Microsoft.UI.System.ThemeSettings.  2 3 4 5 6 7 8

  2. Microsoft Learn, Application.SetColorMode(SystemColorMode) Method. عن الاستدعاء قبل إنشاء عناصر الواجهة، وأن التطبيق لا يتكيّف تلقائيّاً عند تغيّر إعداد النظام حتّى مع تحديد System، وأن وضع اللون الداكن متاح على Windows 11 فما بعده فقط وغير متاح في وضع التباين العالي.  2 3 4

  3. Microsoft Learn, Support Dark and Light themes in Win32 apps. عن تعريف المقدّمة والخلفية في وضع اللون، وأن Windows لا يعرف هل التطبيق يدعم الداكن فيعطي شريط عنوان فاتحاً افتراضيّاً للتوافق، وإجراء كشف الوضع الداكن بأخذ لون المقدّمة بـ UISettings.GetColorValue والحكم على السطوع المدرَك، والتتبع بـ ColorValuesChanged، وتفعيل شريط العنوان الداكن بـ DwmSetWindowAttribute وDWMWA_USE_IMMERSIVE_DARK_MODE (القيمة 20)، وأن السطح كلّه يلزم أن يتبع الداكن.  2 3 4 5 6 7 8 9 10

  4. Microsoft Learn, DWMWINDOWATTRIBUTE enumeration (dwmapi.h). عن أن DWMWA_USE_IMMERSIVE_DARK_MODE يسمح برسم الإطار داكناً عندما يكون إعداد النظام الداكن مفعّلاً وأن كلّ النوافذ تصير فاتحة افتراضيّاً، وتحديد COLORREF لـ DWMWA_BORDER_COLOR وDWMWA_CAPTION_COLOR وDWMWA_TEXT_COLOR والعودة إلى الافتراض بـ DWMWA_COLOR_DEFAULT، والدعم من Windows 11 الإصدار 22000 فما بعده، ودعم DWMWA_SYSTEMBACKDROP_TYPE من الإصدار 22621 فما بعده.  2 3 4

  5. Microsoft Learn, UISettings.ColorValuesChanged Event. عن الحدث الذي يقع عند تغيّر قيم اللون.  2

  6. Microsoft Learn, What’s new in Windows Forms for .NET 9. عن الدعم الأولي التجريبي للوضع الداكن، وتغيّر SystemColors عند تغيّر وضع اللون، وقيم SystemColorMode الثلاث Classic وSystem وDark، واستدعاء Application.SetColorMode في شيفرة التشغيل، وكبت WFO5001.  2 3

  7. Microsoft Learn, What’s new in Windows Forms for .NET 10. عن الدمج الكامل للوضع الداكن وأن SetColorMode لم يعد تجريبيّاً، وأن تحكّمات Win32 المشتركة داخل تحكّم مرسوم ذاتيّاً تبقى فاتحة ما لم تشترك، وأنّه يلزم استدعاء SetStyle(ControlStyles.ApplyThemingImplicitly) داخل تجاوز CreateParams قبل base.CreateParams وأن المنشئ متأخّر.  2 3 4

  8. Microsoft Learn, What’s new in WPF for .NET 9. عن سمة Fluent الداعمة للفاتح/الداكن ولون التمييز، وقيم ThemeMode الأربع Light وDark وSystem وNone والضبط على Application/Window، والتطبيق بقاموس موارد، وأن ضبط ThemeMode من الشيفرة تجريبي ويلزم كبت WPF0001.  2 3 4

  9. Microsoft Learn, What’s new in WPF for .NET 10. عن أن دعم نمط واجهة Fluent ما زال قيد العمل، وإضافة أنماط Fluent لـ DatePicker وGridSplitter وGridView وGroupBox وHyperlink وLabel وNavigationWindow وRichTextBox وTextBox، وإصلاح انهيار مرتبط بـ HighContrast.  2 3

  10. Microsoft Learn, Application.ThemeMode Property. عن التحكّم في وضع تحميل سمة Fluent فاتحاً أو داكناً أو حسب النظام، والتحكّم أيضاً في تطبيق مادّة الخلفية والوضع الداكن على النافذة، وتصميم تزامن ThemeMode وResources لتفادي عدم الاتساق، وسمة Experimental(“WPF0001”) وإمكان الحذف لاحقاً.  2 3 4

  11. Microsoft Learn, High contrast parameter. عن جلب بنية HIGHCONTRAST بـ SPI_GETHIGHCONTRAST عند التهيئة وعند معالجة WM_SYSCOLORCHANGE وتأكيد HCF_HIGHCONTRASTON، وعن نقل كلّ الألوان عند التفعيل إلى مجموعة COLOR_WINDOWTEXT وCOLOR_WINDOW أو COLOR_BTNTEXT وCOLOR_BTNFACE، وحذف صور النقطية خلف النصّ، ورسم الصور متعدّدة الألوان بلون المقدّمة ولون الخلفية.  2 3

  12. Microsoft Learn, High-contrast mode. عن أن Aero نصّ أسود ولون تحديد أزرق فاتح بينما في High Contrast Black يصير لون التحديد أسود وقد يصير نصّاً أسود على أرض سوداء، وأن COLOR_HIGHLIGHTTEXT يُفترض مع COLOR_HIGHLIGHT وCOLOR_WINDOWTEXT مع COLOR_WINDOW، وعدم تثبيت لون النصّ برمجيّاً، وجعل الواجهة لا تعتمد على السمة لأن المستخدم يخصّص الألوان، وإعادة حساب الألوان عند WM_THEMECHANGED، وأن SPI_GETHIGHCONTRAST طريقة التأكيد المدعومة الوحيدة.  2 3

  13. Microsoft Learn, Walkthrough: Creating an Accessible Windows-based Application. عن الحكم بـ SystemInformation.HighContrast، واستخدام ألوان النظام عند التفعيل وإرفاق دليل بصري بالمعلومات المنقولة باللون وحذف الصور خلف النصّ، والتأكيد عند التشغيل وتتبع حدث UserPreferenceChanged، ومثال تبديل ألوان اللصاقة بـ SystemColors.  2 3

  14. W3C / لجنة أساس إمكانية الوصول على الويب (WAIC) ترجمة، Web Content Accessibility Guidelines (WCAG) 2.1 Japanese translation. عن معيار النجاح 1.4.3 (التباين (الحدّ الأدنى)) 4.5:1 للنصّ و3:1 للنصّ الكبير، ومعيار النجاح 1.4.1 (استخدام اللون).  2 3

  15. Microsoft Learn, Color in Windows. عن أن Windows يملك وضعي لون فاتح وداكن، وانعكاس اختيار لون التمييز والسمة على تجربة المستخدم جملة، وضمان التباين ومراعاة تنوّع إدراك اللون.  2

  16. Microsoft Learn, Reference for Windows 11 and Windows 10 settings. عن أن AppsUseLightTheme وSystemUsesLightTheme تحت HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize قيمتا DWORD تمثّلان الفاتح/الداكن للتطبيق وWindows. 

  17. Microsoft Learn, Theming in Windows apps. عن اتباع إعداد النظام عند إزالة RequestedTheme، وتجاوز النظام لـ RequestedTheme عندما يختار المستخدم سمة تباين عالٍ، وعدم تثبيت الألوان برمجيّاً في القوالب المخصّصة واستخدام فرش السمة. 

  18. Microsoft Learn, DwmSetWindowAttribute function (dwmapi.h). عن الدالة التي تضبط سمات رسم DWM لمنطقة غير العميل في النافذة، وأنّها متاحة من Windows Vista فما بعده. 

  19. Microsoft Learn, Retrieve a window handle (HWND). عن طريقة جلب Handle من WindowInteropHelper في WPF. 

  20. Microsoft Learn, DWM_SYSTEMBACKDROP_TYPE enumeration (dwmapi.h). عن أن DWMSBT_MAINWINDOW يعادل Mica في Windows 11 وDWMSBT_TRANSIENTWINDOW يعادل Acrylic، وأن أثر المادّة قد يتغيّر في Windows لاحق، والدعم من Windows 11 الإصدار 22621 فما بعده. 

  21. Microsoft Learn, UISettings.GetColorValue(UIColorType) Method. عن الدالة التي تعيد قيمة اللون لـ UIColorType المحدَّد. 

  22. Microsoft Learn, WM_SETTINGCHANGE message. عن الرسالة التي تُرسل إلى كلّ نوافذ المستوى الأعلى عندما يغيّر SystemParametersInfo إعداداً على مستوى النظام أو يتغيّر إعداد سياسة. 

  23. Microsoft Learn, SystemEvents.UserPreferenceChanged Event. عن الحدث الثابت الذي يقع عند تغيّر إعداد المستخدم، وتسرّب الذاكرة إن لم يُزل المعالج. 

  24. Microsoft Learn, WM_THEMECHANGED message. عن البثّ إلى كلّ النوافذ بعد تفعيل السمة أو إلغائها أو تبديلها، وبطلان مقابض السمة القائمة فلزم إعادة فتحها. 

  25. Microsoft Learn, WM_SYSCOLORCHANGE message. عن الإرسال إلى كلّ نوافذ المستوى الأعلى عند تغيّر إعداد ألوان النظام، ولزوم إعادة صنع الفرش التي تستخدم ألوان النظام وإعادة التوجيه إلى التحكّمات المشتركة. 

  26. Microsoft Learn, Compiler Error WFO5001. عن أن SetColorMode وSystemColorMode محميّان في .NET 9 بوصفهما ميزة تجريبية لغرض التقييم، وأن هذا الخطأ لا ينطبق من .NET 10 فما بعده. 

  27. Microsoft Learn, SystemColors.UseAlternativeColorSet Property. عن أن true تجعل KnownColor للنظام يعيد مجموعة ألوان بديلة (حالياً نسخة الوضع الداكن)، وأنّها ميزة تجريبية SYSLIB5002، وأن KnownColor للنظام يعيد دائماً ألوان Windows الحالية عندما تكون سمة التباين العالي مفعّلة على Windows. 

  28. Microsoft Learn, HIGHCONTRASTW structure (winuser.h). عن HCF_HIGHCONTRASTON (0x00000001) في dwFlags، ولزوم تحديد cbSize عند الاستخدام مع SPI_GETHIGHCONTRAST. 

  29. Microsoft Learn, SystemParameters.HighContrast Property. عن الخاصّية الثابتة في WPF المقابلة لـ SPI_GETHIGHCONTRAST وHCF_HIGHCONTRASTON. 

  30. Microsoft Learn, SystemParameters.StaticPropertyChanged Event. عن الحدث الثابت الذي يقع عند تغيّر أيّ خاصّية في SystemParameters.  2

  31. Microsoft Learn, ThemeSettings Class (Microsoft.UI.System). عن الإنشاء مربوطاً بالنافذة بـ CreateForWindowId واستقبال تغيّر التباين العالي بحدث Changed، وأن تحرير المرجع يُتلف الكائن فيتوقّف وقوع الحدث. 

  32. Microsoft Learn, SystemColors.WindowBrushKey Property. عن أن الإشارة الديناميكية بمفتاح موارد تُحدَّث تلقائيّاً عند تغيّر الفرشاة، وأن الإشارة الثابتة بـ WindowBrush لا تُحدَّث تلقائيّاً. 

  33. Microsoft Learn, ResourceDictionary.ThemeDictionaries Property (Microsoft.UI.Xaml). عن تجهيز قاموس HighContrast أيضاً للتحكّم المخصّص الذي يحمل قاموسي Light وDark، وأن HighContrast مفتاح احتياطي عندما لا توجد سمة تباين عالٍ أخرى، وأن Default يُستخدم عندما لا يوجد ResourceDictionary للسمة، وأن موارد ألوان النظام مثل SystemColorButtonFaceColor تُستخدم في HighContrast. 

  34. Microsoft Learn, Windows app development best practices. عن أن Windows 11 تجنّب الأبيض الخالص والأسود الخالص وحدّث درجات ألطف للعين، وأن سمة الداكن/الفاتح وسيلة تكيّف مع الذوق البصري للمستخدم. 

  35. Microsoft Learn, Color (Windows UX guidelines). عن استخدام اللون تعزيزاً بصريّاً لا وسيلة إبلاغ رئيسة، واختيار ألوان السمة وألوان النظام حسب الغرض واستخدام المقدّمة والخلفية توليفة مقابلة، ومعالجة تغيّر السمة بـ WM_THEMECHANGED، وأن High Contrast Black يقابل Aquatic في Windows 11 وHigh Contrast White يقابل Desert. 

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

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

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

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

حوّلت Windows إلى الوضع الداكن، لكن شريط عنوان تطبيق WinForms الداخلي ما زال أبيض. لماذا؟
لأن Windows لا يملك وسيلة لمعرفة ما إذا كان التطبيق يدعم الوضع الداكن، فيعامل كلّ النوافذ افتراضيّاً وضعاً فاتحاً للتوافق. منطقة غير العميل بما فيها شريط العنوان يرسمها مدير نوافذ سطح المكتب (DWM)، ولا يُرسم الإطار داكناً عندما يكون النظام داكناً إلّا بعد أن يمرّر التطبيق TRUE لـ DWMWA_USE_IMMERSIVE_DARK_MODE (القيمة 20) عبر DwmSetWindowAttribute. دعم هذه السمة موثَّق لـ Windows 11 الإصدار 22000 فما بعده. إن استخدمت Application.SetColorMode في WinForms على .NET 9 فما بعده، أو ThemeMode في WPF على .NET 9 فما بعده، يتولّى الإطار هذا الاستدعاء، فالاستدعاء الذاتي يقتصر على تطبيقات .NET 8 فما قبله و.NET Framework وWin32/MFC. وشريط عنوان أسود فوق منطقة عميل تبقى بيضاء يبدو أشدّ نشازاً. لا تفعّل هذه السمة إلّا عندما تكون جاهزاً لإعادة طلاء التطبيق كلّه داكناً.
هل الوضع الداكن وسمة التباين (التباين العالي) الشيء نفسه؟
لا. الفاتح/الداكن وضع اللون تحت الإعدادات > التخصيص > الألوان، ويستخدم لوحة واسعة تبدّل سطوع المقدّمة والخلفية. سمة التباين تُختار تحت الإعدادات > إمكانية الوصول > سمات التباين، وتستخدم لوحة مقيَّدة بنسبة تباين تبلغ تقريباً 7:1 فما فوق (الأنواع الأربعة المضمَّنة Aquatic وDesert وDusk وNight sky، إضافة إلى ألوان يحرّرها المستخدم). تنصّ وثائق Microsoft صراحة على عدم الخلط بينهما، ولا يتاح الوضع الداكن أثناء تفعيل سمة تباين (لا يقدّم SetColorMode في WinForms الداكن أثناء سمة تباين، وRequestedTheme في XAML تتجاوزه النظام). في التنفيذ احكم أوّلاً هل سمة التباين مفعّلة فإن كانت فأوكل الألوان جملة إلى ألوان النظام، وإلّا فاختر لوحة الفاتح أو الداكن. هذا ترتيب الأولوية.
ما أقصر طريق لجعل تطبيق WinForms يدعم الوضع الداكن؟
على .NET 9 فما بعده أقصر طريق استدعاء Application.SetColorMode(SystemColorMode.System) قبل Application.Run في Program.cs. في .NET 9 كانت ميزة تجريبية فلزم كبت WFO5001 في ملفّ المشروع، ومن .NET 10 تُستخدم بلا كبت. استدعاء SetColorMode يبدّل SystemColors إلى مجموعة بديلة للوضع الداكن، وتُرسم التحكّمات القياسية وفقها. التنبيهات ثلاثة. الأوّل أن الوضع الداكن متاح على Windows 11 فما بعده فقط ويُعطَّل أثناء سمة تباين. الثاني أنّه حتّى مع SystemColorMode.System لا يتبع التطبيق تغيّر إعداد Windows أثناء التشغيل (يسري عند التشغيل التالي). الثالث أنّ التحكّم المرسوم ذاتيّاً إن استخدم تحكّمات Win32 المشتركة مثل أشرطة التمرير يلزم تجاوز CreateParams واستدعاء SetStyle(ControlStyles.ApplyThemingImplicitly, true) قبل base.CreateParams (المنشئ متأخّر).
ماذا يفعل تطبيق WPF ليتبع الوضع الداكن؟
من .NET 9 فما بعده تضمّ WPF سمة جديدة وفق تصميم Fluent في Windows 11، ويكفي كتابة ThemeMode="System" على عنصر Application في App.xaml لتُحمَّل سمة Fluent الموافقة لإعداد الفاتح/الداكن في Windows. يتحكّم ThemeMode أيضاً في تغميق النافذة (شريط العنوان) وتطبيق مادّة الخلفية. لكن قراءة ThemeMode وكتابته من الشيفرة تجريبية حتّى في .NET 10 (WPF0001)، ونمط Fluent نفسه «ما زال قيد العمل» في وثائق .NET 10. عند اعتماده في تطبيق أعمال قرّر بعد تقييم أن التحكّمات المستخدمة لا تنهار تحت Fluent. إن بقيت على السمة التقليدية (وكذا .NET 8 فما قبله و.NET Framework) فجهّز ResourceDictionary للفاتح وآخر للداكن وبدّلهما في MergedDictionaries، واجعل جانب XAML يشير بـ DynamicResource، واستخدم UISettings.ColorValuesChanged لكشف التبديل. لشريط العنوان خذ HWND من WindowInteropHelper في SourceInitialized واستدعِ DwmSetWindowAttribute.
لماذا تختفي النصوص أو يتعذّر قراءتها تحت سمة التباين (التباين العالي)؟
السبب النموذجي تثبيت الألوان برمجيّاً، أو كسر زوج المقدّمة والخلفية من ألوان النظام. تحت سمة التباين يمكن للمستخدم تحرير ألوان الخلفية والنصّ والرابط وغيرها بحرّية، فتسقط كلّ فرضية من نوع «النصّ أسود» أو «صفّ التحديد أزرق فاتح». إن ثبّت الخلفية وحدها على #E6E6E6 مثلاً صارت المقدّمة بيضاء في بعض السمات، فيركب نصّ أبيض فوق رمادي فاتح ويتعذّر القراءة. المبادئ ثلاثة: الحكم على الحالة بـ SPI_GETHIGHCONTRAST (في WinForms SystemInformation.HighContrast، وفي WPF SystemParameters.HighContrast)، واستبدال الألوان كلّها بأزواج ألوان النظام الصحيحة (WindowText مع Window، وButtonText مع ButtonFace، وHighlightText مع Highlight)، وترك الصور خلف النصّ والرسوم متعدّدة الألوان والرسم بلون المقدّمة ولون الخلفية فقط. GrayText لحالة التعطيل، وHotlight للارتباط التشعّبي دون غيره. يصل التغيير عبر WM_SYSCOLORCHANGE وWM_THEMECHANGED (في .NET عبر SystemEvents.UserPreferenceChanged)، فاحسب الألوان هناك وأعد الرسم.

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

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

غو كومورا

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

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

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