Dark mode ו-Contrast themes באפליקציות Windows — title bar כהה של DWM, מעקב theme ב-WinForms/WPF, וציור תחת High Contrast

· עודכן בתאריך: · · Dark Mode, Contrast Themes, High Contrast, DWM, WinForms, WPF, Windows 11, נגישות, אפליקציות עסקיות, Windows

היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 2 Sep 2026)
פרסום ראשון

“החלפנו את מחשבי המשרד ל-Windows 11, והעובדים שמשתמשים ב-dark mode מתלוננים שאפליקציית העסק שלנו היא היחידה שה-title bar שלה לבן מסנוור.” “עובד עם ראייה ירודה הפעיל contrast theme, ותצוגת הסטטוס במסך ההזמנות נעלמה.” שתי התלונות האלה נשמעות אצלנו יותר בשנה-שנתיים האחרונות.

הראשונה באה מהגדרת Colors ב-Windows 11, השנייה מהגדרות Accessibility, אבל למפתח הן נראות כמו אותה בעיה: האפליקציה לא עוקבת אחרי ה-theme. ובאמת, הבסיס של התיקון משותף. לא מקבעים צבעים; קוראים את הגדרת המערכת, מבחינים בשינויים, ומציירים מחדש. שלוש הנקודות האלה.

המאמר הקודם, “מבוא לנגישות באפליקציות Windows”, כיסה איך screen readers קוראים אפליקציה (UI Automation) ואת היסודות של שמות, מקלדת וצבע. הוא נגע במעקב אחרי contrast themes, אבל לא כיסה את מצב הצבע light/dark עצמו. המאמר הזה הוא אחיו. הוא מחבר את שני צירי מצב הצבע (light/dark) ו-contrast themes ברמת המימוש, בסדר הזה: ציור title bar על ידי DWM (Desktop Window Manager), מעקב אחרי theme המערכת ב-WinForms/WPF, וציור תחת contrast theme. קהל היעד הוא מפתחים שבונים ומתחזקים אפליקציות עסקיות ב-WinForms, WPF או Win32. סביבת הבסיס היא Windows 11 (title bar כהה דורש build 22000 ואילך) ו-WinForms/WPF ב-.NET 9/10 (ב-.NET Framework 4.8 וב-.NET 8 חלק מהדברים מממשים ידנית). רמת הקושי היא בינונית.

הזרימה של המאמר הזהמבנה המאמר, שמחבר לפי הסדר את שני צירי ה-theme, למה חלונות ברירת המחדל היא light, title bar כהה של DWM, מנגנון הזיהוי והמעקב, המימושים ב-WinForms וב-WPF, ציור תחת contrast theme, בחירת מדיניות, ואימותמסדרים את שני צירי ה-themeלמה ברירת המחדל היא lighttitle bar כהה של DWMזיהוי ומעקבמימוש WinForms/WPFציור תחת contrast themeבחירת מדיניות ואימות

איור 1: המאמר הזה רץ בקו אחד מסידור ה-themes דרך מנגנונים, מימוש, contrast themes ואימות.

1. קודם המסקנות

  • ל-Windows יש שני צירי theme. Light/dark (מצב הצבע) תחת Settings > Personalization > Colors, ו-Settings > Accessibility > Contrast themes. האחרון הוא לוח מוגבל ליחס contrast של בערך 7:1 ומעלה, והוא דבר אחר מ-light/dark. Dark mode אינו זמין כל עוד contrast theme פעיל. סדר העדיפות לזיהוי הוא “contrast theme, ואחר כך light/dark”.12
  • Title bar של אפליקציה קיימת נשאר לבן כי זו ברירת המחדל לתאימות. ל-Windows אין דרך לדעת אם אפליקציה תומכת ב-dark mode, ולכן היא מתייחסת לכל חלון כ-light כברירת מחדל.3
  • מה שהופך את ה-title bar לכהה הוא DWMWA_USE_IMMERSIVE_DARK_MODE (ערך 20) דרך DwmSetWindowAttribute. מעבירים BOOL TRUE והמסגרת מצוירת כהה כשהמערכת כהה. התמיכה המתועדת היא Windows 11 build 22000 ואילך.43
  • קוראים את המצב הנוכחי עם UISettings.GetColorValue ומקבלים שינויים דרך ColorValuesChanged. ההליך הרשמי של Microsoft: אם ה-foreground (צבע הטקסט ברירת המחדל) בהיר, המצב הוא dark. האירוע אינו מובטח להגיע ב-UI thread, ולכן מחזירים ל-UI לפני שמצירים מחדש.35
  • WinForms קיבל Application.SetColorMode ב-.NET 9, והוא הפסיק להיות ניסיוני ב-.NET 10. קוראים לו עם SystemColorMode.System לפני Application.Run. יש לו שלוש מגבלות: Windows 11 בלבד, מבוטל כל עוד contrast theme פעיל, ואין מעקב אחרי שינוי הגדרה בזמן ריצה.672
  • WPF קיבל את Fluent theme ו-ThemeMode ב-.NET 9. ThemeMode="System" עוקב אחרי המערכת וגם שולט בהכהיית החלון. עם זאת, מניפולציה מקוד עדיין ניסיונית ב-.NET 10 (WPF0001), וסגנונות Fluent הם “בעבודה”. אם נשארים ב-theme הקלאסי, מחליפים ResourceDictionary של light ו-dark שמפנים אליהם דרך DynamicResource.8910
  • תחת contrast theme, ממפים צבעים לזוגות system-color הנכונים, מורידים תמונות מאחורי טקסט, ומציירים גרפיקה רב-צבעית בשני צבעי foreground ו-background. מזהים עם SPI_GETHIGHCONTRAST (SystemInformation.HighContrast ב-WinForms, SystemParameters.HighContrast ב-WPF); ההתראות הן WM_SYSCOLORCHANGE / WM_THEMECHANGED (SystemEvents.UserPreferenceChanged ב-.NET).111213
  • תמיכה ב-dark mode אינה תחליף לתמיכה בנגישות. גם לוח כהה צריך יחס contrast של 4.5:1, והעברת מידע ביותר מצבע בלבד נדרשת בכל theme.1415

במשפט אחד: תמיכה ב-theme פירושה לאסוף צבעים במקום אחד, לקרוא את הגדרת המערכת, להבחין בשינויים ולצייר מחדש; אבל תחת contrast theme, משאירים הכל לזוגות צבעי המערכת.

ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 28, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle

2. ל-“theme” יש שני צירים — Light/Dark ו-Contrast themes

2.1. Light/Dark (מצב צבע)

מצב הצבע תחת Settings > Personalization > Colors ב-Windows הוא ההגדרה שקובעת את בהירות ה-foreground וה-background בכל מערכת ההפעלה ובכל האפליקציות. התיעוד של Microsoft מגדיר light כ-“foreground כהה על רקע בהיר” ו-dark כ-“foreground בהיר על רקע כהה”, ומוסיף ש-foreground כאן פירושו “צבע הטקסט ברירת המחדל”. ב-dark mode ה-foreground (טקסט) בהיר והרקע כהה.3

ההגדרה נשמרת ב-Registry תחת HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize כערכי DWORD AppsUseLightTheme (מצב האפליקציה) ו-SystemUsesLightTheme (מצב Windows עצמה), והיא מופיעה במדריך ההגדרות של Microsoft.16 כפי שיתואר בהמשך, עם זאת, הדרך הקנונית לקרוא אותה מאפליקציה היא המחלקה UISettings של WinRT.

2.2. Contrast themes (High Contrast)

Contrast theme, שנבחר תחת Settings > Accessibility > Contrast themes, משתמש בלוח מוגבל ליחס contrast של בערך 7:1 ומעלה, וקיים למשתמשים שצריכים הפרדה חזותית חזקה בין foreground ל-background. ל-Windows 11 יש ארבעה מובנים, Aquatic, Desert, Dusk ו-Night sky, והמשתמש יכול לא רק לבחור אחד מהם אלא גם לערוך בנפרד את צבעי הרקע, הטקסט, ה-hyperlink, הטקסט המבוטל, הטקסט הנבחר והכפתור. Left Alt + left Shift + Print Screen מחליף contrast theme במהירות, ו-Aquatic מוחל אם אף אחד לא נבחר.1

התיעוד של Microsoft קובע במפורש: “אל תבלבלו contrast themes עם light ו-dark themes”. Light/dark משתמש בלוח רחב ואינו מותאם ל-contrast מקסימלי.1 והחלק החשוב הוא ש-dark mode אינו זמין כל עוד contrast theme פעיל. Application.SetColorMode של WinForms אינו מספק dark mode במהלך contrast theme, ו-RequestedTheme של XAML נדרס על ידי המערכת.217

2.3. סדר העדיפות

המימוש באפליקציה לכן עוקב אחרי הסדר הזה. קודם מחליטים אם contrast theme פעיל; אם כן, משאירים הכל לצבעי המערכת. אם לא, בוחרים את לוח light או dark.

שני צירי ה-theme וסדר העדיפותסדר ההחלטה שבו האפליקציה משאירה הכל לזוגות צבעי המערכת אם contrast theme פעיל, ואחרת קוראת את מצב הצבע light/dark ובוחרת את לוח האפליקציהכןלאLightDarkContrast theme פעיל?משאירים לזוגות צבעי המערכתמצב הצבע?לוח lightלוח darkDark mode אינו זמין

איור 2: שמים את החלטת ה-contrast theme ראשונה, ובוחרים את לוח light או dark רק כשאין contrast theme פעיל.

3. למה אפליקציות קיימות נשארות לבנות ב-dark mode

חלון מורכב משני אזורים: אזור ה-non-client, שעשוי מ-title bar, המסגרת וכפתורי הכיתוב, ו-אזור ה-client, שהאפליקציה מציירת. מאז Windows Vista, אזור ה-non-client מורכב ומצויר על ידי DWM (Desktop Window Manager), והאפליקציה מציינת מאפיינים של איך הוא מצויר דרך DwmSetWindowAttribute.18

התיעוד של Microsoft מסביר בכנות למה אפליקציות קיימות נשארות לבנות. “Windows לא יודע אם אפליקציה יכולה לתמוך ב-dark mode, ולכן הוא מניח שהיא לא יכולה מסיבות של תאימות לאחור.” Frameworks כמו WinUI ו-Windows App SDK מטפלים ב-dark mode באופן native, אבל אפליקציות Win32 בדרך כלל לא תומכות ב-dark mode, ולכן Windows נותן להן title bar בהיר כברירת מחדל.3

שני אזורי החלון ומי מצייר אותםאזור ה-non-client שעשוי מ-title bar וממסגרת מצויר על ידי DWM, ואזור ה-client מצויר על ידי האפליקציה או UI framework, ולכן תמיכה ב-dark mode נחוצה בשניהםחלון ברמה העליונהאזור non-client (title bar, מסגרת)אזור client (תוכן החלון)מורכב ומצויר על ידי DWMמצויר על ידי האפליקציה או ה-frameworkמונחה דרך DwmSetWindowAttributeלוח הצבעים של האפליקציה עצמה

איור 3: DWM מצייר את ה-title bar והאפליקציה מציירת את התוכן, כך שתמיכה ב-dark mode צריכה גם הנחיה ל-DWM וגם את לוח האפליקציה.

שתי מסקנות נובעות מזה. ראשית, כדי להפוך את ה-title bar לכהה, האפליקציה חייבת לבקש במפורש מ-DWM. שנית, מה שנהיה כהה כתוצאה מהבקשה הוא רק ה-title bar; אזור ה-client חייב להיות מצויר מחדש על ידי האפליקציה עצמה. התיעוד גם אומר ש-“כדי לתמוך ב-dark mode במלואו, כל משטח האפליקציה צריך לעקוב אחרי ה-dark theme”, ומציין שהמדריך הרשמי מכסה רק זיהוי ו-title bar, לא איך לצייר מחדש את אזור ה-client.3 אפליקציה עם title bar שחור ותוכן לבן נראית פחות טבעית מאחת שנשארת לבנה לכל אורכה.

איך ברירת המחדל נהיית lightWindows לא יודע אם אפליקציה תומכת ב-dark mode, ולכן לתאימות ברירת המחדל היא light, ורק כשהאפליקציה מעבירה TRUE דרך מאפיין DWM הוא מצייר את המסגרת לפי הגדרת dark של המערכתלאכןWindows לא יכול לדעת אם האפליקציה תומכתברירת המחדל היא light, לתאימותהאם האפליקציה העבירה TRUE?תמיד מסגרת בהירהמצויר לפי הגדרת המערכת

איור 4: בלי לדעת אם האפליקציה תומכת, Windows ברירת המחדל היא light ועוקב אחרי המערכת רק כשהאפליקציה אומרת במפורש.

4. Title bar כהה של DWM — DwmSetWindowAttribute

4.1. DWMWA_USE_IMMERSIVE_DARK_MODE

המאפיין שהופך את ה-title bar לכהה הוא DWMWA_USE_IMMERSIVE_DARK_MODE. ה-enumeration DWMWINDOWATTRIBUTE מתאר אותו כך: “מאפשר למסגרת החלון של החלון הזה להיות מצוירת בצבעי dark mode כשהגדרת dark mode של המערכת מופעלת. מסיבות תאימות, כל החלונות ברירת המחדל שלהם היא light mode בלי קשר להגדרת המערכת. הפרמטר pvAttribute מצביע לערך מסוג BOOL. TRUE כדי לכבד dark mode לחלון, FALSE כדי תמיד להשתמש ב-light mode. הערך הזה נתמך החל מ-Windows 11 Build 22000.”4

כלומר, TRUE לא אומר “תעשה כהה”; זו הרשאה שאומרת “מותר לצייר כהה אם המערכת כהה”. אם האפליקציה מוכנה לצבוע את אזור ה-client בכהה, העברת TRUE היא כל מה שצריך כדי שה-title bar יעקוב אחרי הגדרת המערכת. לעומת זאת, אם האפליקציה מתוכננת תמיד להציג ב-light mode (מדיניות “light קבוע” שתוארה בהמשך), השארת ברירת המחדל FALSE בסדר.

קוד ה-C++ במדריך הרשמי לובש את הצורה הבאה. הוא אפילו כולל את הצעד של הגדרת ערך 20 בעצמכם ל-SDKs ישנים שה-headers שלהם חסרים את הקבוע.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 (build 22000) ואילך? מניח manifest שמצהיר 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: ה-title bar מותר להיות מצויר כהה כשהמערכת כהה
void ApplyTitleBarTheme(HWND hwnd, bool honorDarkMode)
{
    if (!IsWindows11OrGreater())
    {
        // התמיכה המתועדת היא build 22000 ואילך. מתחת לזה, לא קוראים ועוקבים אחרי ברירת המחדל (light)
        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))
    {
        // כשל ב-OS נתמך הוא חריג. לא בולעים אותו בשקט; רושמים את ה-HRESULT ומציפים אותו
        LogWarning(L"DwmSetWindowAttribute(DWMWA_USE_IMMERSIVE_DARK_MODE) failed: 0x%08X", hr);
    }
}

מילה על למה הקריאה מגודרת לפי גרסת ה-OS. התמיכה המתועדת היא Windows 11 build 22000 ואילך.4 דיווחים שאותו ערך עובד ב-Windows 10 אינם נדירים, אבל תצוגה של אפליקציה עסקית לא צריכה להיות תלויה בהתנהגות לא מתועדת. אם התכנון הוא “מנסים את הקריאה ומוותרים בכשל”, אז כשהקריאה במקרה מצליחה ב-Windows 10, ה-title bar נהיה כהה מעל התנהגות לא מתועדת. מתחת ל-build 22000, לא קוראים ועוקבים אחרי ברירת המחדל המתועדת של title bar בהיר; ב-OS נתמך, רושמים את ה-HRESULT של כשל ומציפים אותו. זה הכל. באינטרנט גם מסתובב הליך ישן שמשתמש בערך 19, וטכניקה שקוראת ל-ordinal exports של uxtheme.dll כדי להכהות את ה-common controls, אבל שניהם APIs לא מתועדים, ואף אחד לא מבטיח אותם כשעדכון משנה את ההתנהגות.

4.2. מתי לקרוא — כשה-HWND חי, ובכל פעם שהוא נוצר מחדש

DwmSetWindowAttribute נקרא על HWND, ולכן הוא חייב לרוץ אחרי ש-window handle נוצר. וטופס WinForms יכול לקבל handle שנוצר מחדש, למשל כש-ShowInTaskbar משתנה. ה-HWND החדש אחרי יצירה מחדש לא נושא מאפיין, כך שהמקום לקרוא אינו ה-constructor אלא המקום שרץ בכל פעם ש-handle נוצר: OnHandleCreated ב-WinForms, SourceInitialized ב-WPF.

מתי לקרוא ל-DwmSetWindowAttributeמגדירים את מאפיין DWM אחרי ש-window handle נוצר, מגדירים אותו שוב על ה-handle החדש כשה-handle נוצר מחדש, ומגדירים אותו שוב כשמגיעה התראת שינוי themeHandle נוצר מחדשהתראת שינוי themeHWND נוצרמגדירים את מאפיין DWMמוצג

איור 5: מאפיין DWM קשור ל-HWND, ולכן מגדירים אותו שוב בכל יצירה ובכל יצירה מחדש.

ה-P/Invoke ב-WinForms נראה כך (לדרך הבטוחה לכתוב DllImport, ראו “קריאה בטוחה ל-Win32 APIs מ-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()
    {
        // התמיכה המתועדת היא build 22000 ואילך. מתחת לזה, לא קוראים ועוקבים אחרי ברירת המחדל (light).
        // הבדיקה הזו עובדת גם ב-.NET Framework. ב-.NET Framework, עם זאת, בלי manifest שמצהיר
        // 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) = תמיד light
        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 ואילך, ה-framework לוקח על עצמו את הכהיית החלון (התיעוד של ThemeMode קובע שהוא “גם שולט בהחלת backdrop material ו-dark mode על החלון”).10 קריאה כפולה לא מזיקה, אבל היא מטשטשת מי אחראי, ולכן בוחרים באחד מהם.

ב-WPF, ה-HWND מתייצב ב-SourceInitialized. לוקחים את ה-handle מ-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. צבע title bar, צבע טקסט, צבע מסגרת ו-backdrop material

Windows 11 הוסיף מאפיינים שמציינים את צבע ה-title bar עצמו, מעבר לבחירה הבינארית של dark או light.

מאפיין ערך משמעות Build נתמך
DWMWA_USE_IMMERSIVE_DARK_MODE 20 מציירים את המסגרת כהה כשהמערכת כהה (BOOL) 22000
DWMWA_BORDER_COLOR 34 צבע מסגרת החלון (COLORREF). DWMWA_COLOR_NONE מסיר את המסגרת 22000
DWMWA_CAPTION_COLOR 35 צבע ה-title bar (COLORREF) 22000
DWMWA_TEXT_COLOR 36 צבע טקסט הכותרת (COLORREF) 22000
DWMWA_SYSTEMBACKDROP_TYPE 38 backdrop material שמצויר על ידי המערכת (Mica או Acrylic) 22621

לשלושת מאפייני הצבע, העברת DWMWA_COLOR_DEFAULT (0xFFFFFFFF) מחזירה את ברירת המחדל של המערכת. שימו לב שלצבע המסגרת, “זו אחריות האפליקציה לשנות את הצבע בתגובה לשינויי מצב כמו הפעלת חלון”.4 ה-backdrop material מצוין עם ה-enumeration DWM_SYSTEMBACKDROP_TYPE; ב-Windows 11, DWMSBT_MAINWINDOW תואם ל-Mica ו-DWMSBT_TRANSIENTWINDOW ל-Acrylic, אבל התיעוד קובע ש-“האפקט של החומר עשוי להשתנות בגרסאות עתידיות של Windows”.20

חושבים בזהירות איפה זה שייך באפליקציה עסקית. ברגע שצובעים את ה-title bar בצבע מותג, האחריות להבטיח את ה-contrast של טקסט הכותרת וכפתורי הכיתוב על הצבע הזה עוברת אליכם. מעל שני המצבים dark ו-light, הצירופים עם פעיל ולא-פעיל מכפילים. לרוב האפליקציות העסקיות התשובה הנכונה היא “לעקוב אחרי ברירת המחדל של המערכת (רק מגדירים ערך 20 ל-TRUE)”, וצבע מותג הוא אפשרות כשבאמת צריך אותו.

איך מחליטים על צבע ה-title barמעקב אחרי ברירת המחדל של המערכת דורש רק הגדרת ערך 20 ל-TRUE, אבל צביעת צבע מותג מעבירה לאפליקציה את האחריות להבטיח contrast של טקסט וכפתורי כיתוב ולנהל צבעים פעילים ולא-פעילים, ושחזור מעביר DWMWA_COLOR_DEFAULTלעקוב אחרי ברירת המחדל של המערכתלצבוע צבע מותגצבע ה-title bar?רק מגדירים ערך 20 ל-TRUEמציינים את מאפייני הצבע (34 עד 36)מבטיחים בעצמכם contrast של טקסט וכפתוריםמנהלים בעצמכם פעיל/לא-פעילמשחזרים עם COLOR_DEFAULT

איור 6: בחירת צבע מותג מעבירה לאפליקציה את האחריות ל-contrast ולניהול מצב, ולכן לרוב האפליקציות העסקיות מעקב אחרי ברירת המחדל הוא התשובה הנכונה.

5. זיהוי ומעקב אחרי ה-theme של המערכת — קוראים, מבחינים, מציירים מחדש

העבודה של לגרום לאזור ה-client לעקוב אחרי ה-theme מתפרקת לשלושה חלקים: קוראים את ההגדרה הנוכחית, מבחינים בשינויים, ו-מציירים מחדש.

שלושת הצעדים של זיהוי ומעקבהלולאה של קריאת מצב הצבע הנוכחי עם UISettings בהפעלה, הבחנה בשינויים דרך התראות כמו ColorValuesChanged, החזרה ל-UI thread, וציור מחדש של לוח האפליקציהקוראים: UISettings.GetColorValueמציירים מחדש: מחילים שוב את הלוחמבחינים: ColorValuesChangedמחזירים ל-UI threadמגדירים שוב גם את מאפיין DWM

איור 7: קוראים בהפעלה, מבחינים דרך התראות, מחזירים ל-UI thread, ומציירים מחדש: הלולאה הזו היא שלד המעקב אחרי theme.

5.1. קוראים — UISettings ו-“אם ה-foreground בהיר, זה dark”

ההליך הרשמי של Microsoft משתמש במחלקת WinRT Windows.UI.ViewManagement.UISettings. לוקחים את צבע ה-foreground (צבע הטקסט ברירת המחדל) עם GetColorValue(UIColorType::Foreground), מעריכים את הבהירות הנתפסת בחשבון מספרים שלמים כדי להחליט אם הוא “בהיר”, ומסיקים dark mode אם ה-foreground בהיר. התיעוד מציין שהנוסחה אינה מודל בהירות קפדני, רק קירוב שמספיק לסיווג light ו-dark.321

UISettings היא מחלקת WinRT, אבל אפליקציות C# WPF ו-WinForms יכולות לקרוא לה ישירות אם ה-TargetFramework נושא גרסת Windows SDK, כמו net8.0-windows10.0.19041.0 (לאיך זה עובד, ראו “WinRT הוא COM”). אפשר גם לקרוא את AppsUseLightTheme ישירות מה-Registry, אבל ה-Registry הוא המקום שבו ההגדרה נשמרת, לא חוזה API; אם קוראים אותו, מתייחסים ל-UISettings כמקור האמת ושומרים את ה-Registry לאבחון.

דרכים לקרוא את מצב הצבעהנתיב הקנוני הוא לקחת את צבע ה-foreground דרך WinRT UISettings ולסווג אותו כ-light או dark; ערך Registry AppsUseLightTheme הוא מקום האחסון ויש לשמור אותו לאבחוןLight או dark עכשיו?UISettings.GetColorValueRegistry AppsUseLightThemeשופטים את הבהירות הנתפסת של ה-foregroundבהיר פירושו dark modeמקום אחסון. שומרים לאבחון

איור 8: נתיב הקריאה הקנוני הוא UISettings; ה-Registry הוא רק איפה שההגדרה נשמרת.

5.2. מבחינים — ColorValuesChanged לא מגיע ב-UI thread

UISettings משמש גם לזיהוי שינויים. האירוע ColorValuesChanged נורה כשערך צבע משתנה, והמדריך הרשמי משתמש באירוע הזה כדי לעקוב אחרי שינויי הגדרה.53 זהירות מעשית אחת חלה כאן. האירוע הזה אינו מובטח להגיע ב-UI thread. מחזירים ל-UI thread עם Dispatcher של WPF, Control.Invoke של WinForms, או SynchronizationContext שעובד בשניהם, לפני שנוגעים בפקד כלשהו. עבודה עם ה-UI thread מסוכמת ב-“UI thread ו-async/await ב-WPF/WinForms”.

האירוע גם נורה כשצבע ה-accent משתנה. אם רוצים לצייר מחדש רק כש-light/dark משתנה, מעריכים מחדש בכל אירוע ומודיעים רק כשהתוצאה שונה מפעם קודמת. ולפי סדר העדיפות בפרק 2, לא מסתכלים על בהירות ה-foreground כל עוד contrast theme פעיל. Contrast theme עם רקע כהה כמו Aquatic יש לו foreground בהיר, כך שבהירות לבדה הייתה מסווגת אותו בטעות כ-“dark”. מתייחסים ל-contrast theme כמצב עצמאי ומחליטים עליו קודם.

המחלקה הבאה אוספת את סדר ההחלטה הזה למקום אחד. ה-SynchronizationContext שמשמש להחזרה ל-UI thread ובדיקת ה-contrast theme (SystemInformation.HighContrast ב-WinForms, SystemParameters.HighContrast ב-WPF; ראו פרק 8) מועברים על ידי הקורא. שימו לב מתי יוצרים אותה. ב-WinForms, בנקודה של Program.Main אין עדיין message loop ואין Control, כך ש-SynchronizationContext.Current הוא null. מעבירים SynchronizationContext.Current אחרי שהפקדים קיימים, למשל מ-constructor של הטופס או מ-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 של ה-UI thread. מעבירים SynchronizationContext.Current
    //     אחרי שהפקדים קיימים, או DispatcherSynchronizationContext ב-WPF
    // 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: contrast theme קודם. Contrast theme עם רקע כהה
        // יש לו foreground בהיר, כך שבהירות לבדה הייתה מסווגת אותו בטעות כ-"dark"
        if (_isHighContrast()) return ThemeState.HighContrast;
        // אותה בדיקה כמו במדריך הרשמי: dark אם ה-foreground (צבע הטקסט ברירת המחדל) בהיר
        var fg = _settings.GetColorValue(UIColorType.Foreground);
        bool isDark = (5 * fg.G + 2 * fg.R + fg.B) > 8 * 128;
        return isDark ? ThemeState.Dark : ThemeState.Light;
    }

    // קוראים מ-UI thread. אפשר גם לקרוא מנתיבי התראה אחרים כמו UserPreferenceChanged
    public void Refresh()
    {
        if (_disposed) return;
        var next = Read();
        // בין light ל-dark, לא מודיעים כשהמצב לא השתנה, כדי להתעלם משינויי accent-color בלבד.
        // במהלך contrast theme זו החריגה: המצב נשאר HighContrast גם כשהמשתמש עורך
        // את צבעי ה-theme, ולכן מודיעים גם לאותו מצב כדי שצבעי המערכת ייקראו שוב
        if (next == Current && next != ThemeState.HighContrast) return;
        Current = next;
        Changed?.Invoke(this, EventArgs.Empty);
    }

    private void OnColorValuesChanged(UISettings sender, object args)
    {
        // לא מובטח להגיע ב-UI thread, ולכן מחזירים ל-UI לפני שמחליטים ומודיעים.
        // קריאה שמגיעה אחרי Dispose (כבר בתור) מתעלמת על ידי הדגל ב-Refresh
        _ui.Post(_ => Refresh(), null);
    }

    public void Dispose()
    {
        // ביטול הרישום רק עוצר משלוחים עתידיים; קריאות שכבר נשלחו ל-UI thread נשארות.
        // מגדירים את הדגל כדי שהקריאות שנשארו יתעלמו מעצמן (קוראים לזה ב-UI thread)
        _disposed = true;
        _settings.ColorValuesChanged -= OnColorValuesChanged;
    }
}

ברמת Win32, WM_SETTINGCHANGE נשלח לכל חלון ברמה העליונה כשהגדרה משתנה,22 וב-.NET הוא מגיע כ-SystemEvents.UserPreferenceChanged.23 החלפת visual style (כולל הפעלת contrast theme) מביאה WM_THEMECHANGED,24 ושינוי צבע מערכת מביא WM_SYSCOLORCHANGE.25 במקום לכתוב טיפול נפרד לכל סוג התראה, קוראים לאותה שגרת “קוראים ומציירים מחדש” איזו התראה שמגיעה; זה קשה יותר לשבור, ויש לו אותה צורה כמו המעקב אחרי contrast theme שהוצג במאמר הקודם. במונחי SystemThemeWatcher למעלה, זה אומר לקרוא ל-Refresh() גם מ-handlers של UserPreferenceChanged ו-StaticPropertyChanged.

נתיבי התראת שינוי theme ו-threadsColorValuesChanged מ-UISettings עשוי להגיע מחוץ ל-UI thread ומוחזר, WM_SETTINGCHANGE מגיע כ-SystemEvents.UserPreferenceChanged, ו-WM_THEMECHANGED ו-WM_SYSCOLORCHANGE מגיעים ל-window procedure. כולם מתכנסים לאותה שגרת החלה מחדשColorValuesChangedמחזירים ל-UI threadUserPreferenceChangedקוראים ומציירים מחדשWM_THEMECHANGED וכו'

איור 9: יש כמה נתיבי התראה, אבל כולם מתכנסים לאותה שגרת “קוראים ומציירים מחדש”.

5.3. מציירים מחדש — אוספים צבעים במקום אחד

התנאי שמאפשר ציור מחדש הוא ש-צבעים נאספים במקום אחד. אם Color.White ו-#FFFFFF מפוזרים בטפסים וב-XAML, אי אפשר למנות את המקומות לצייר מחדש. ב-WinForms, יוצרים מחלקת “לוח” (שני מופעים, אחד light ואחד dark) והפקדים לוקחים ממנה צבעים בהפעלה ובכל התראה. ב-WPF, אוספים צבעים ב-ResourceDictionary ומפנים אליהם מ-XAML עם DynamicResource. ב-WinUI, המבנה הזה מסופק מההתחלה כ-ThemeDictionaries.

המבנה שאוסף צבעים במקום אחדהאפליקציה מחזיקה לוח light אחד ולוח dark אחד, וכל מסך לוקח את הצבעים מהלוח שנבחר למצב הנוכחי, כך שאפשר למנות את יעדי הציור מחדשLightDarkהמצב הנוכחיאיזה?לוח lightלוח darkכל מסך ופקדColor.White מפוזראי אפשר למנות מה לצייר מחדש

איור 10: עם הלוח במקום אחד אפשר למנות ציור מחדש; עם צבעים hardcoded מפוזרים אי אפשר.

עבודת “איסוף הצבעים” הזו משתלמת ישירות בתמיכה ב-contrast theme ובבדיקות יחס ה-contrast שיתוארו בהמשך. העלות הגדולה ביותר של תמיכה ב-dark mode אינה קריאות ה-API אלא הניקוי הזה.

6. מימוש ב-WinForms

6.1. .NET 9/10 — Application.SetColorMode

WinForms קיבל תמיכה ראשונית ב-dark mode ב-.NET 9, והיא “שולבה במלואה” ב-.NET 10. Application.SetColorMode מקבל שלושה ערכים.67

  • SystemColorMode.Classic — ברירת המחדל. Light, כמו קודם.
  • SystemColorMode.System — לעקוב אחרי הגדרת light/dark של Windows.
  • SystemColorMode.Dark — dark.

קוראים לו לפני Application.Run, לפני שנוצר אלמנט UI כלשהו. ב-.NET 9 זו הייתה יכולת ניסיונית, כך שזו הייתה שגיאת קומפילציה אלא אם WFO5001 דוכא בקובץ הפרויקט; מ-.NET 10 השגיאה כבר לא מופיעה.26

static class Program
{
    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();
        Application.SetColorMode(SystemColorMode.System); // קוראים לפני יצירת UI כלשהו
        Application.Run(new MainForm());
    }
}

כשמצב הצבע משתנה, System.Drawing.SystemColors עובר לצבעים התואמים, והפקדים הסטנדרטיים מצוירים בהתאם.6 מתחת למכסה, המאפיין הניסיוני SystemColors.UseAlternativeColorSet (SYSLIB5002) “גורם לערכי KnownColor של המערכת להחזיר סט צבעים חלופי (כרגע גרסת dark mode)”; כי צבעי המערכת של Win32 עצמם לא משתנים עם הגדרת light/dark, .NET נושא את הסט החלופי בצד שלו. אותו תיעוד גם אומר ש-כש-contrast theme פעיל, תמיד מוחזרים צבעי Windows הנוכחיים.27

זוכרים את שלוש המגבלות הכתובות בתיעוד של SetColorMode.2

  1. מצב הצבע dark זמין רק ב-Windows 11 ואילך.
  2. Dark mode אינו זמין כש-contrast theme פעיל.
  3. גם עם SystemColorMode.System, האפליקציה לא עוקבת אוטומטית אחרי שינוי בהגדרת Windows בזמן שהיא רצה.

השלישית נוטה לייצר שאלות תמיכה באפליקציות עסקיות. כותבים בתיעוד למשתמש שהמצב נקבע לפי הגדרת Windows בהפעלה ונכנס בהפעלה הבאה. אם חייבים לעקוב אחרי החלפה בזמן ריצה, צריך תכנון שיוצר מחדש את הטפסים, וכמעט אף פעם זה לא שווה את זה.

הזרימה והמגבלות של SetColorModeSetColorMode נקרא לפני Application.Run, SystemColors עובר לסט החלופי, והפקדים הסטנדרטיים עוקבים. יש לו שלוש מגבלות: Windows 11 בלבד, מבוטל במהלך contrast theme, ואין מעקב אחרי שינויי הגדרה בזמן ריצהSetColorMode (System)לפני Application.RunSystemColors עובר לסט החלופיהפקדים הסטנדרטיים עוקביםWindows 11 בלבדמבוטל במהלך contrast themeלא עוקב אחרי שינויים בזמן ריצה

איור 11: SetColorMode נכנס פעם אחת לפני ההפעלה ויש לו שלוש מגבלות מתועדות.

6.2. Owner-drawn controls ו-ApplyThemingImplicitly

הפקדים הסטנדרטיים עוקבים אחרי מצב הצבע של האפליקציה, אבל התיעוד של .NET 10 מפרט שני מקרים חריגים. אם פקד שאתם מרכיבים ומציירים בעצמכם משתמש ב-Win32 common controls כמו פסי גלילה, הם נשארים בהירים אלא אם הם עושים opt-in במפורש. לעומת זאת, אם יורשים פקד קיים שעוקב אחרי ה-theme ורוצים שליטה מלאה בציור בעצמכם, עושים opt-out.7

בשני המקרים, עוקפים את Control.CreateParams וקוראים ל-SetStyle(ControlStyles.ApplyThemingImplicitly, true/false) לפני קריאת base.CreateParams. המלכודת ב-API הזה היא שה-constructor של מחלקת הבסיס קורא ל-CreateParams, כך שה-constructor שלכם מאוחר מדי.7

public partial class GanttChartControl : Control
{
    protected override CreateParams CreateParams
    {
        get
        {
            // מגדירים לפני קריאת base.CreateParams. ה-constructor מאוחר מדי
            SetStyle(ControlStyles.ApplyThemingImplicitly, true);
            return base.CreateParams;
        }
    }
}
מתי אפשר להגדיר ApplyThemingImplicitlyApplyThemingImplicitly נקבע בנקודה שבה constructor של מחלקת הבסיס קורא ל-CreateParams, כך שצריך לקרוא ל-SetStyle לפני base.CreateParams בתוך override של CreateParams, וה-constructor של המחלקה הנגזרת מאוחר מדיConstructor של מחלקת הבסיסקורא ל-CreateParamsSetStyle חייב לקרות עד כאןConstructor של המחלקה הנגזרתקריאה כאן מאוחרת מדי

איור 12: ApplyThemingImplicitly חייב להיקבע לפני שה-constructor של מחלקת הבסיס קורא ל-CreateParams.

Owner drawing עצמו (ציור GDI+ ב-OnPaint) עוקב אחרי הסט החלופי כל עוד הוא משתמש ב-SystemColors / SystemBrushes / SystemPens. כל מקום שצובע עם Color.White מוחלף בלוח שתואר למעלה, גם כאן.

6.3. .NET Framework 4.8 ו-.NET 8 ומטה — מצהירים על “light קבוע”

בסביבות בלי SetColorMode, אין תמיכה סטנדרטית ב-dark mode. יש שתי אפשרויות.

  1. מצהירים על light קבוע. משאירים את מאפיין DWM בברירת המחדל FALSE (תמיד title bar בהיר) ומשאירים את אזור ה-client כמו שהוא. תמיכה ב-contrast theme (פרק 8) עדיין חובה.
  2. מממשים תמיכה מלאה בעצמכם. מאחדים את הלוח, קוראים ועוקבים עם UISettings, מגדירים את מאפיין DWM ל-TRUE, ומציירים מחדש הכל כולל המראה של ה-common controls.

אפשרות 2 נוטה להסתיים ב-“בעיקר כהה, אבל בהיר במקומות”, כי האפליקציה לא יכולה לשלוט במלואו בציור של Win32 common controls (פסי גלילה, headers, כפתורי הרחבת עץ, וכן הלאה). כפי שהמדריך הרשמי אומר, “כל המשטח צריך לעקוב”;3 dark mode חצי-גמור הוא חוויה גרועה יותר מ-light קבוע. לנכסים קיימים, בוחרים באפשרות 1, מצהירים ש-“האפליקציה הזו מציגה ב-light mode”, ועוברים ל-SetColorMode כשמגררים ל-.NET 10. זו המדיניות המציאותית, והקלה ביותר להסביר.

אפשרויות WinForms לפי runtimeב-.NET 10 ואילך משתמשים ב-SetColorMode, ב-.NET 9 באותו API עם WFO5001 מדוכא, וב-.NET 8 ומטה או ב-.NET Framework בוחרים בין הצהרה על light קבוע לבין מימוש תמיכה מלאה בעצמכם עד ה-common controls.NET 10 ואילך.NET 9.NET 8 ומטה / .NET Frameworkמומלץאם מוכנים לזהRuntime?SetColorMode (System)SetColorMode + דיכוי WFO5001מה לעשות?מצהירים על light קבועתמיכה מלאה בעצמכםה-common controls נשארים

איור 13: ב-runtimes בלי SetColorMode, הצהרה על light קבוע היא ברירת המחדל המציאותית.

7. מימוש ב-WPF

7.1. .NET 9/10 — Fluent theme ו-ThemeMode

WPF ב-.NET 9 מגיע עם theme חדש שעוקב אחרי Fluent design של Windows 11, עם תמיכה ב-light/dark ובצבע ה-accent. יש שתי דרכים להחיל אותו: להגדיר את מאפיין ThemeMode, או להוסיף את resource dictionary של PresentationFramework.Fluent ל-MergedDictionaries.8

ThemeMode מקבל ארבעה ערכים, Light / Dark / System / None (ברירת המחדל; ה-theme הקלאסי 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 theme למשאבים; התיעוד קובע שהוא “גם שולט בהחלת backdrop material ו-dark mode על החלון”. כלומר, WPF מטפל במאפיין DWM מפרק 4. ThemeMode ו-Resources גם מתוכננים לעבוד בסנכרון, מה שהתיעוד מסביר כדי להימנע מחוסר עקביות שבו החלון כהה אבל הפקדים בפנים בהירים.10

מה ThemeMode ב-WPF עושההגדרת ThemeMode ל-System טוענת את מילוני Fluent theme שתואמים להגדרת Windows למשאבים וגם שולטת בהחלת dark mode ו-backdrop material על החלוןThemeMode=Systemקוראים את הגדרת Windowsטוענים את מילוני Fluent למשאביםמכהים את החלון ומחילים את ה-backdropנשארים בסנכרון עם Resources כדי להימנע מחוסר עקביות

איור 14: ThemeMode שולט בטעינת מילוני Fluent ובהכהיית החלון יחד.

יש שתי זהירויות לדעת לפני אימוץ. ראשית, קריאה וכתיבה של ThemeMode מקוד היא יכולת ניסיונית, וגישה אליו מייצרת את השגיאה WPF0001. אם מדכאים אותה אפשר לכתוב Application.Current.ThemeMode = ThemeMode.Dark, אבל ה-API reference עדיין נושא [Experimental("WPF0001")] ב-.NET 10, עם ההערה שהוא “עשוי להיות מוסר בעתיד”.810 שנית, כיסוי סגנון Fluent עדיין “בעבודה” ב-.NET 10. .NET 10 הוסיף סגנונות ל-DatePicker, GridSplitter, GroupBox, TextBox ואחרים, ותיקן קריסות שקשורות ל-HighContrast,9 מה שאומר, לקריאה ההפוכה, ש-Fluent theme ב-.NET 9 חסר אותם. לפני שמחליטים לאמץ, מאשרים על מכונה אמיתית שהפקדים שאפליקציית העסק משתמשת בהם (DataGrid ופקדי צד שלישי בפרט) לא נשברים תחת Fluent.

7.2. מעקב על ה-theme הקלאסי — החלפת ResourceDictionary

ב-WPF בלי Fluent (או ב-.NET 8 ומטה, או ב-.NET Framework), אין תמיכה סטנדרטית ב-dark mode. צבעי המערכת של Win32 לא משתנים עם הגדרת light/dark, כך שהפניה ל-SystemColors של WPF לא תהפוך דבר לכהה. המבנה למעקב אחרי ה-theme בעצמכם הוא כדלקמן.

  1. מגדירים צבעים ומברשות עם אותם מפתחות ב-Themes/Light.xaml ל-light וב-Themes/Dark.xaml ל-dark.
  2. מפנים אליהם מ-XAML עם DynamicResource, כמו {DynamicResource App.WindowBackgroundBrush} (StaticResource מקובע בזמן טעינה ולא עוקב אחרי החלפה).
  3. בהתראה מ-SystemThemeWatcher של פרק 5, מחליפים את המילון המתאים ב-MergedDictionaries. במהלך contrast theme, איזה מילון שתשימו, ה-trigger מסעיף 8.4 מחליף את הצבעים בצבעי מערכת, ולכן שמים את ה-light.
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);
        // המילון הכהה רק כש-dark. במהלך contrast theme, משאירים לצבעי המערכת (סעיף 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; // מחליפים במקום, באותה עמדה
        }
    }
}
החלפת resource dictionaries ב-WPFמגדירים צבעים עם אותם מפתחות במילון light ובמילון dark, מפנים אליהם מ-XAML עם DynamicResource, ומחליפים את המילון ב-MergedDictionaries בהתראת שינוי theme כך שההפניות מתעדכנותהתראת שינוי themeמחליפים את המילון ב-MergedDictionariesLight.xaml (אותם מפתחות)Dark.xaml (אותם מפתחות)הפניות DynamicResource מתעדכנותהפניות StaticResourceשומרות את הערך מזמן הטעינה

איור 15: מחליפים מילונים עם אותם מפתחות, ורק הפניות DynamicResource עוקבות.

התבניות של הפקדים הסטנדרטיים (רקעי כפתורים, צבעי פס גלילה) נושאות את צבעי ה-theme הקלאסי, כך שגם כאן יהיו מקומות שבהם “המשטחים של האפליקציה עצמה כהים אבל הפקדים הסטנדרטיים בהירים”. מעריכים את עבודת דריסת הסגנון של כל פקד שצריך, ואז משווים מול אימוץ Fluent או light קבוע.

7.3. ה-title bar

אם משתמשים ב-ThemeMode, WPF מטפל בזה. אם עוקבים אחרי ה-theme בעצמכם על ה-theme הקלאסי, משתמשים בהגדרת מאפיין DWM מ-OnSourceInitialized שמוצגת בסעיף 4.2, ומגדירים אותה שוב בהתראות מ-SystemThemeWatcher.

8. ציור תחת Contrast theme — שומרים על זוגות צבעי המערכת

8.1. זיהוי והתראה

ב-Win32, מעבירים SPI_GETHIGHCONTRAST ל-SystemParametersInfo כדי לקבל מבנה HIGHCONTRAST, ובודקים את הביט HCF_HIGHCONTRASTON של dwFlags. cbSize חייב להיות מוגדר לפני הקריאה.1128 Microsoft ממקמת את זה כ-“הדרך היחידה הנתמכת לבדוק אם high contrast דלוק”.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;
}

לכל framework יש מאפיין שעוטף את הקריאה הזו.

סביבה זיהוי התראת שינוי
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

ה-walkthrough של נגישות WinForms מבקש לבדוק HighContrast בהפעלה ולהגיב לשינויים דרך UserPreferenceChanged.13 SystemParameters.HighContrast של WPF ממופה ל-SPI_GETHIGHCONTRAST ול-HCF_HIGHCONTRASTON,29 ושינויים במאפיינים הסטטיים מוכרזים דרך StaticPropertyChanged.30 ThemeSettings של WinUI 3 נוצר קשור לחלון עם CreateForWindowId ונרשמים לאירוע Changed שלו, אבל שימו לב ש-האירועים נעצרים אלא אם ממשיכים להחזיק הפניה לאובייקט.31

נתיבי זיהוי contrast themeWin32 SPI_GETHIGHCONTRAST הוא שיטת הזיהוי היחידה הנתמכת, ו-SystemInformation.HighContrast ב-WinForms, SystemParameters.HighContrast ב-WPF, ו-ThemeSettings.HighContrast ב-WinUI 3 מסופקים על ידי כל framework כעטיפות סביבוSPI_GETHIGHCONTRAST (שיטת הזיהוי היחידה)WinForms SystemInformationWPF SystemParametersWinUI 3 ThemeSettingsWin32: קוראים ישירות

איור 16: שורש הזיהוי הוא Win32 API אחד, ולכל framework יש מאפיין שעוטף אותו.

8.2. עקרונות ציור — זוגות foreground ו-background

“High contrast parameter” של Microsoft מפרט שלושה דברים שאפליקציה צריכה לעשות כש-high contrast דלוק.11

  1. ממפים כל צבע לזוג אחד של צבעי foreground ו-background. משתמשים ב-GetSysColor עם הזוג COLOR_WINDOWTEXT ו-COLOR_WINDOW, או הזוג COLOR_BTNTEXT ו-COLOR_BTNFACE.
  2. מורידים תמונות bitmap שמוצגות מאחורי טקסט. הן מכשול חזותי למשתמשים שצריכים high contrast.
  3. מציירים תמונות רב-צבעיות בצבעי foreground ו-background שמשמשים לטקסט.

ה”זוג” הוא הלב. המדריך של Windows 8 ואילך מסביר ש-COLOR_HIGHLIGHTTEXT מתוכנן להיות משולב עם רקע COLOR_HIGHLIGHT ו-COLOR_WINDOWTEXT עם רקע COLOR_WINDOW, ומבקש לא לקבע צבעי טקסט ו, כי משתמשים מתאימים אישית את הצבעים, לבנות UI שלא תלוי ב-theme הפעיל.12 הדוגמה של אותו מדריך, “ב-Aero, טקסט תמיד שחור וצבע הבחירה כחול בהיר, אבל ב-High Contrast Black צבע הבחירה שחור. אם מניחים טקסט שחור ומשתמשים בצבע הבחירה של המערכת, מקבלים טקסט שחור על שחור”, היא בדיוק תלונת “תצוגת הסטטוס נעלמה” מהפתיחה.

ההנחיה ל-contrast themes של Windows 11 ממסדרת את הזיווגים בטבלה.1

שימוש Foreground Background
כותרות, גוף טקסט, רשימות, גבולות, UI לא-אינטראקטיבי SystemColorWindowText SystemColorWindow
Hyperlinks SystemColorHotlight SystemColorWindow
UI מבוטל או לא-פעיל SystemColorGrayText SystemColorWindow
נבחר, hover, לחוץ, בתהליך SystemColorHighlightText SystemColorHighlight
UI אינטראקטיבי כמו כפתורים SystemColorButtonText SystemColorButtonFace

גם מה לא לעשות מפורט. לא משתמשים ב-GrayText לטקסט משלים או רמז (הוא למצב disabled בלבד); לא משתמשים ב-Hotlight לשום דבר מלבד hyperlinks; לא מערבבים foregrounds ו-backgrounds לא תואמים; לא בוחרים צבעים לפי מראה (משתמשים באמת משנים אותם). יש גם הנחיית עיצוב שרקעי עמודים, חלוניות ו-popups מבוססים על SystemColorWindow, כך שמשטחים סמוכים מקבלים אותו צבע רקע, ורק הגבולות שחשובים מופרדים במסגרת שמשמשת רק תחת contrast themes (2px מומלץ ל-flyouts ולדיאלוגים).1

איך שבירת הזוג הופכת טקסט לבלתי קריאהנחה שטקסט שחור ושימוש בצבע הבחירה של המערכת רק לרקע הבחירה נותנת שחור על שחור ב-High Contrast Black, שבו צבע הבחירה שחור. לקיחת foreground ו-background כזוג שומרת על טקסט קריא גם כשהמשתמש עורך את הצבעיםמניחים שטקסט שחוררק רקע הבחירה משתמש בצבע הבחירה של המערכתב-High Contrast Black צבע הבחירה שחורטקסט שחור על שחורלוקחים foreground ו-background כזוגקריא גם כשהמשתמש עורך את הצבעיםהמשתמש עורך את הצבעים

איור 17: שימוש בצבע מערכת בצד אחד בלבד יכול לתת שחור על שחור, אבל לקיחת הזוג נשארת קריאה גם כשהצבעים נערכים.

החלטות ציור תחת contrast themeכש-contrast theme פעיל, ממפים צבעים לזוגות צבעי המערכת, מורידים תמונות מאחורי טקסט, מציירים תמונות רב-צבעיות בשני צבעי foreground ו-background, ולא משתמשים בצבעים hardcodedContrast theme פעילממפים צבעים לזוגותמורידים תמונות מאחורי טקסטמציירים גרפיקה רב-צבעית בשני צבעיםבלי צבעים hardcoded

איור 18: ציור תחת contrast theme פעיל מתכנס לארבע נקודות: מיפוי, השמטה, שני צבעים, ובלי hardcoding.

8.3. מימוש ב-WinForms

הפקדים הסטנדרטיים של WinForms עוקבים אחרי צבעי המערכת כל עוד ForeColor / BackColor נשארים בברירת המחדל. רק המקומות עם צבעים מותאמים ו-owner drawing מוחלפים לפי הבדיקה. הדוגמה ב-walkthrough לוקחת תווית שהיא צהובה על כחול בדרך כלל ומחזירה אותה ל-SystemColors.Window / SystemColors.WindowText תחת high contrast.13 זה מסתכם בהוספת ענף contrast-theme למבנה הלוח מלמעלה.

using Microsoft.Win32;

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

    public OrderForm()
    {
        InitializeComponent();
        // הפקדים קיימים עכשיו, כך ש-WindowsFormsSynchronizationContext במקום
        _ui = SynchronizationContext.Current
              ?? throw new InvalidOperationException("יש ליצור את הטופס הזה ב-UI thread.");
        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; // לוח light/dark (פרק 5)
            statusLabel.BackColor = p.PanelBackground;
            statusLabel.ForeColor = p.PanelForeground;
            headerPanel.BackgroundImage = Properties.Resources.HeaderPattern;
        }
    }

    private void OnUserPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e)
    {
        // גם האירוע הזה אינו מובטח להגיע ב-UI thread. מחזירים ל-UI, ואז מעריכים מחדש בלי סינון לפי קטגוריה
        _ui.Post(_ =>
        {
            if (IsDisposed) return;
            ApplyColorScheme();
        }, null);
    }

    // אירוע סטטי, כך שהטופס דולף אלא אם מנתקים. מנתקים ב-Dispose(bool), שרץ גם
    // בנתיבים שבהם הטופס מושמד בלי להיסגר (אם קיים Dispose(bool) שנוצר ב-designer, שמים שם)
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged;
        }
        base.Dispose(disposing);
    }
}

Owner drawing ב-OnPaint משתמש במברשות מערכת ששומרות על הזוג, כמו SystemBrushes.Window / SystemPens.WindowText, וגרפיקה רב-צבעית כמו נקודה צבעונית שמייצגת סטטוס מוחלפת במסגרת ובטקסט (“Running”, “Stopped”) בצבע ה-foreground. העברת מידע ביותר מצבע בלבד היא אותה נקודה כמו Success Criterion 1.4.1 שכוסה במאמר הקודם.

פיצול סכמת הצבע ב-WinFormsApplyColorScheme נקרא בהפעלה ובכל UserPreferenceChanged; אם SystemInformation.HighContrast הוא true משאירים לזוגות צבעי המערכת ומסירים את תמונת הרקע, ואם false לוקחים צבעים מלוח light/darkTrueFalseהפעלה / UserPreferenceChangedApplyColorSchemeSystemInformation.HighContrast?משאירים לזוגות SystemColorsמסירים את תמונת הרקעלוקחים צבעים מלוח light/dark

איור 19: ב-WinForms אותה שגרה נקראת בהפעלה ובכל התראה, ותחת contrast theme משאירים לצבעי המערכת.

8.4. מימוש ב-WPF

SystemColors של WPF מתעדכנים אוטומטית כשמברשת משתנה אם מפנים ל-מפתח משאב כמו WindowBrushKey דרך DynamicResource (הפניה סטטית שמשתמשת ב-WindowBrush ישירות לא מתעדכנת).32 כדי לשנות את המראה רק במהלך contrast theme, מפנים לערך של SystemParameters.HighContrast מ-DataTrigger. עם זאת, SystemParameters.HighContrast הוא מאפיין סטטי, כך שעל עצמו הוא אינו מקור binding חי. מכינים proxy קטן אחד שנרשם ל-StaticPropertyChanged, מחזיק את הערך, ומודיע דרך INotifyPropertyChanged, ועושים bind עם המופע הזה כ-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 וה-trigger של contrast themeהפניה למפתחות משאב של SystemColors דרך DynamicResource עוקבת אחרי שינויי מברשת אוטומטית, ו-trigger שקשור ל-IsHighContrast על proxy שנרשם ל-StaticPropertyChanged מגיב להחלפה בזמן ריצה ועובר לצבעים ששומרים על הזוג. הפניה ישירה ל-WindowBrush לא מתעדכנתמפנים ל-WindowBrushKey דרך DynamicResourceעוקב אחרי שינויי מברשת אוטומטיתStaticPropertyChangedIsHighContrast על ה-proxyDataTrigger מגיבעוברים לצבעים ששומרים על הזוגמפנים ל-WindowBrush ישירותלא מתעדכן

איור 20: WPF עוקב אחרי החלפה בזמן ריצה דרך הפניות דינמיות למפתחות משאב ו-binding ל-proxy שמעביר שינויים במאפיין הסטטי.

אם משתמשים ב-Fluent theme, זוכרים ש-.NET 10 כלל תיקוני קריסה שקשורים ל-HighContrast,9 ועושים אימות תחת contrast theme לתנאי אימוץ.

8.5. מימוש ב-WinUI 3

ב-WinUI 3, הפקדים הסטנדרטיים עוקבים אחרי light, dark ו-contrast themes מההתחלה, וצבעי האפליקציה עצמה מוגדרים ב-ResourceDictionary.ThemeDictionaries תחת המפתחות Default (dark), Light ו-HighContrast. תחת HighContrast, לא מקבעים צבעים; מפנים לצבעי מערכת דינמיים כמו SystemColorWindowColor דרך ThemeResource. פקד מותאם שיש לו Light/Dark חייב תמיד גם HighContrast, ו-HighContrast הוא מפתח ה-fallback שמשמש כשלא נמצא theme high-contrast בשם אחר.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, מופעל כברירת מחדל. הוא כופה טקסט לבן ורקע highlight שחור כדי לשמור על contrast, וההנחיה ממליצה ש-ברגע שהכנתם theme dictionaries שמשתמשים בצבעי המערכת נכון, מגדירים אותו ל-None כדי שהסגנונות שלכם יוחלו.1

איך WinUI פותר ThemeDictionariesלפי ה-theme הנוכחי, אחד מהמילונים Default (dark), Light ו-HighContrast נבחר, ותחת HighContrast מפנים לצבעי מערכת דינמיים דרך ThemeResource. HighContrast הוא מפתח ה-fallback כשאין theme high-contrast בשםDarkLightContrast themeה-theme הנוכחי?מילון Defaultמילון Lightמילון HighContrastמפנים למשאבי SystemColor דרך ThemeResourceFallback כשאין theme בשם

איור 21: ב-WinUI המילון לכל theme נבחר אוטומטית, ומילון HighContrast מפנה לצבעי המערכת.

9. תמיכה ב-dark mode אינה תחליף לתמיכה בנגישות

דיווח על תמיכה ב-dark mode כ-“עשינו את עבודת הנגישות” הוא טעות. הקשר בין השניים אפשר לפרוס כך.

  • תקן יחס ה-contrast חל על הלוח הכהה באותה מידה. WCAG Success Criterion 1.4.3 דורש 4.5:1 לטקסט ו-3:1 לטקסט גדול, וזה לא משתנה כשהרקע כהה.14 עיצוב כהה ששם טקסט אפור-בינוני על רקע אפור כהה יש לו אותה בעיה כמו “אפור בהיר על לבן” ב-light mode.
  • הימנעות משחור טהור ולבן טהור היא העיצוב של Windows 11. שיטות העבודה המומלצות של Microsoft מסבירות ש-Windows 11 עבר משחור טהור ולבן טהור לגוונים שקלים יותר לעיניים.34 לעומת זאת, רקע #000000 ב-dark mode מוביל חלק מהאנשים להתלונן על halation, אפקט פריחה שנגרם מ-contrast מוגזם מול טקסט בהיר.
  • התאמה לגיוון ראיית צבע נחוצה בלי קשר ל-theme. הנחיית הצבע של Microsoft מבקשת להשתמש בצבע כחיזוק חזותי ולא כאמצעי התקשורת העיקרי, ולעולם לא לעשות את הצירוף של אדום וירוק להבחנה היחידה.3515
  • Contrast themes הם דרישה עצמאית מ-dark mode. כפי שפרק 2 הסביר, dark mode אינו זמין כל עוד contrast theme פעיל, כך שגם אם תמיכת dark mode שלכם מושלמת, היא לעולם לא מגיעה למשתמשי contrast themes.
הקשר בין dark mode לנגישותתמיכה ב-dark mode היא עניין של העדפה חזותית וסביבה, בעוד שדרישות הנגישות של יחס contrast, העברת מידע ביותר מצבע בלבד, ותמיכה ב-contrast theme חייבות להתקיים בנפרד בלי קשר ל-themeלא תחליף לתמיכה ב-dark modeהעדפה וסביבהנגישותיחס contrast 4.5:1לא צבע בלבדתמיכה ב-contrast theme

איור 22: תמיכה ב-dark mode מטפלת בהעדפה ובסביבה; דרישות הנגישות חייבות להתקיים בנפרד.

מצד שני, עבודת “איסוף צבעים במקום אחד” מפרק 5 היא היסוד של שניהם. עם הלוח במקום אחד, אפשר למנות מה למדוד ליחס contrast גם ב-light וגם ב-dark, וענף ה-contrast theme יכול להיכתב באותו מקום. משתמשים בתמיכה ב-dark mode כהזדמנות לאחד את הלוח, ובודקים יחסי contrast ו-contrast themes תוך כדי. זה הסדר עם התשואה הטובה ביותר על ההשקעה.

10. בחירת מדיניות — המלצות לפי סוג אפליקציה

סוג אפליקציה מדיניות מומלצת
WinForms חדש (.NET 10) משתמשים ב-SetColorMode(System). מבססים owner drawing על SystemColors, ועושים opt-in לפקדים מותאמים שמכילים common controls עם ApplyThemingImplicitly
WPF חדש (.NET 9/10) מאמצים ThemeMode="System" אחרי בדיקה איך הפקדים שבהם משתמשים מצוירים ואיך האפליקציה מתנהגת תחת contrast themes. אם זה קשה, theme קלאסי ועוד החלפת מילונים
WinForms/WPF קיים (.NET Framework 4.8, .NET 8 ומטה) מצהירים על “light קבוע” ומשאירים את מאפיין DWM בברירת המחדל (FALSE). תמיכה ב-contrast theme חובה; עוברים לתמיכה ב-dark mode כשמגררים ל-.NET 10
WinUI 3 עוקב אחרי המערכת כברירת מחדל. מגדירים את צבעי האפליקציה עצמה ב-ThemeDictionaries כולל HighContrast, ומגדירים HighContrastAdjustment ל-None
Win32 / MFC מאפיין DWM + לוח עצמי + חישוב מחדש ב-WM_THEMECHANGED / WM_SYSCOLORCHANGE. המדריך הרשמי מכסה רק זיהוי ו-title bar; ציור מחדש של ה-common controls מחוץ להיקף שלו
הנתיב מנכסים קיימים לתמיכה ב-dark modeנכסים קיימים קודם מצהירים על light קבוע, משלימים תמיכה ב-contrast theme בלי לוותר, מאחדים את הלוח כהכנה, ואז מגררים ל-.NET 10 ועוברים ל-SetColorMode או ThemeMode. Dark mode חצי-גמור הוא חוויה גרועה יותר מ-light קבוע, ולכן הנתיב הזה לא נלקחלא נלקחמצהירים על light קבוע (עכשיו)תמיכה ב-contrast theme (חובה)מאחדים את הלוח (הכנה)מגררים ל-.NET 10עוברים ל-SetColorMode / ThemeModeDark mode חצי-גמורחוויה גרועה יותר מ-light קבוע

איור 23: נכסים קיימים מתחילים מ-light קבוע, עוברים דרך תמיכה ב-contrast theme ואיחוד לוח, ועוברים לתמיכה ב-dark mode כשמגררים ל-.NET 10.

“Light קבוע” אינו הפסד. זו התנהגות ברירת המחדל של Windows עצמה, והתנהגות מתועדת. אפליקציה שעקבית ב-light טובה בהרבה למשתמשים מאשר dark mode חצי-גמור שמגיע עם “רק ה-title bar שחור” או “רק פסי הגלילה לבנים”. עם זאת, תמיכה ב-contrast theme היא הדבר האחד שאי אפשר “לקבע”. היא נופלת תחת ההתאמה הסבירה שכוסתה במאמר הקודם: זה עניין של האם האפליקציה שמישה, לא עניין של העדפת theme.

11. רשימת בדיקה לאימות

אחרי שהעבודה נעשתה, מאמתים על מכונה אמיתית בסדר הבא. כל צעד אפשר להחליף מאפליקציית Settings בשניות.

  1. מחליפים light/dark בזמן שהאפליקציה רצה. משנים את המצב תחת Settings > Personalization > Colors, ומאשרים שגם ה-title bar וגם אזור ה-client עוקבים, או שהאפליקציה מתנהגת כמפורט עם “נכנס בהפעלה הבאה” (SetColorMode ב-WinForms לא עוקב).
  2. מפעילים יצירת handle מחדש. ב-WinForms, מחליפים ShowInTaskbar בזמן ריצה ומאשרים שמאפיין ה-title bar נשמר.
  3. מנסים את כל ארבעת contrast themes. מחליפים עם left Alt + left Shift + Print Screen, ומאשרים בכל אחד מ-Aquatic, Desert, Dusk ו-Night sky שטקסט, גבולות, שורות נבחרות, פריטים מבוטלים וקישורים קריאים.1
  4. עורכים את צבעי contrast theme. משתמשים באמת משנים את הצבעים. עורכים את הרקע לצבע קיצוני כדי לשטוף החוצה צבעים hardcoded שנשארו.
  5. בודקים את הלוגים. מאשרים שכשל של DwmSetWindowAttribute או SystemParametersInfo ב-OS נתמך נרשם, ושב-Windows 10 מתחת ל-build 22000 האפליקציה עולה ב-light בלי לקרוא למאפיין DWM.
  6. מודדים את יחסי ה-contrast. גם ב-light וגם ב-dark, בודקים כל צירוף של צבע טקסט ורקע בלוח מול 4.5:1.14
צעדי אימות לתמיכה ב-themeמאמתים על מכונה אמיתית בסדר הזה: החלפת light/dark בזמן ריצה, יצירת handle מחדש, ארבעת contrast themes, עריכת צבעי ה-theme, בדיקת לוגי כשל, ומדידת יחסי contrastמחליפים light/dark בזמן ריצהיצירת handle מחדשארבעת contrast themesעורכים את צבעי ה-themeבודקים את לוגי הכשלמודדים את יחסי ה-contrast

איור 24: האימות מתחיל בהחלפת הגדרות ונסגר בבדיקת הלוגים ויחסי ה-contrast.

12. סיכום

  • ל-themes של Windows יש שני צירים, light/dark ו-contrast themes, ו-dark mode אינו זמין כל עוד האחרון פעיל. מזהים קודם את ה-contrast theme.
  • Title bar של אפליקציה קיימת לבן כי זו ברירת המחדל לתאימות; מעבירים TRUE ל-DWMWA_USE_IMMERSIVE_DARK_MODE (ערך 20, Windows 11 build 22000 ואילך) דרך DwmSetWindowAttribute, והוא מצויר כהה כשהמערכת כהה. מגדירים אותו בכל פעם שה-HWND נוצר, ורושמים כשלים.
  • מחליטים על המצב הנוכחי מבהירות צבע ה-foreground מ-UISettings.GetColorValue, מבחינים בשינויים עם ColorValuesChanged, מחזירים ל-UI thread, ומציירים מחדש. שומרים את הצבעים נאספים במקום אחד.
  • WinForms: Application.SetColorMode(SystemColorMode.System) ב-.NET 9/10. יודעים את שלוש המגבלות (Windows 11 בלבד, מבוטל במהלך contrast theme, אין מעקב אחרי שינויים בזמן ריצה) ו-ApplyThemingImplicitly לפקדים מותאמים.
  • WPF: ThemeMode="System" ב-.NET 9/10. מניפולציה מקוד ניסיונית ו-Fluent בעבודה, ולכן מעריכים לפני אימוץ. על ה-theme הקלאסי, החלפת מילונים ועוד DynamicResource.
  • תחת contrast theme, מזהים עם משפחת SPI_GETHIGHCONTRAST, ממפים צבעים לזוגות צבעי המערכת, מורידים תמונות מאחורי טקסט, ומציירים גרפיקה רב-צבעית בשני צבעים. GrayText הוא למצב disabled, Hotlight לקישורים בלבד.
  • תמיכה ב-dark mode אינה תחליף לתמיכה בנגישות. 4.5:1 עדיין חל ב-dark mode, מידע לא צריך להסתמך על צבע בלבד, ותמיכה ב-contrast theme נדרשת בנפרד.
  • לנכסים קיימים, הצהרה על “light קבוע” היא התשובה המציאותית, ותמיכה ב-contrast theme היא הדבר האחד שאי אפשר לקבע.

כצעד ראשון מומלץ, בוחרים מסך ראשי אחד, קודם מפעילים contrast theme עם left Alt + left Shift + Print Screen ומסתכלים עליו, חוזרים עם אותם מקשים, ואז מחליפים את הגדרת הצבע של Windows ל-dark (dark mode אינו זמין כל עוד contrast theme פעיל, ולכן מנסים את השניים בנפרד). תוך כמה דקות תראו “איפה האפליקציה שלכם מחזיקה את הצבעים”.

מאמרים קשורים

תחומי ייעוץ קשורים

KomuraSoft LLC מטפלת בתמיכה ב-dark mode לאפליקציות עסקיות WinForms/WPF (איחוד הלוח, הערכת מיגרציה ל-SetColorMode / ThemeMode ב-.NET 9/10, שילוב מאפיין DWM), באבחון ותיקון שבירת תצוגה תחת contrast themes, ובייעוץ על מעקב theme בנכסי Win32/MFC. להתחיל מהשלב של “עובדים התלוננו ברגע שעברו ל-dark mode” זה בסדר.

קישורים

  1. Microsoft Learn, Contrast themes. ש-contrast themes משתמשים בלוח מוגבל של בערך 7:1 ומעלה ואין לבלבל אותם עם light ו-dark themes; ארבעת ה-themes Aquatic, Desert, Dusk ו-Night sky ועריכת הצבעים שלהם; החלפה עם left Alt + left Shift + Print Screen; זוגות foreground/background ושימושי משאבי SystemColor; שימוש ב-GrayText למצב disabled בלבד וב-Hotlight לקישורים בלבד; שבירה מצבעים hardcoded; מסגרות גבול; HighContrast ב-ThemeDictionaries; הגדרת HighContrastAdjustment ל-None; וזיהוי עם Microsoft.UI.System.ThemeSettings.  2 3 4 5 6 7 8

  2. Microsoft Learn, Application.SetColorMode(SystemColorMode) Method. על קריאה לפני יצירת אלמנטי UI, שהאפליקציה לא מסתגלת אוטומטית כשהגדרת המערכת משתנה גם עם System, ושמצב הצבע dark זמין רק ב-Windows 11 ואילך ואינו זמין במצב high contrast.  2 3 4

  3. Microsoft Learn, Support Dark and Light themes in Win32 apps. על הגדרת foreground ו-background במצבי הצבע; ש-Windows נותן title bar בהיר כברירת מחדל לתאימות כי הוא לא יכול לדעת אם אפליקציה תומכת ב-dark mode; ההליך של לקיחת צבע ה-foreground עם UISettings.GetColorValue וסיווג light או dark לפי בהירות נתפסת כדי לזהות dark mode; מעקב עם ColorValuesChanged; הפעלת title bar כהה עם DwmSetWindowAttribute ו-DWMWA_USE_IMMERSIVE_DARK_MODE (ערך 20); ושכל המשטח צריך לעקוב אחרי dark mode.  2 3 4 5 6 7 8 9 10

  4. Microsoft Learn, DWMWINDOWATTRIBUTE enumeration (dwmapi.h). ש-DWMWA_USE_IMMERSIVE_DARK_MODE מאפשר למסגרת להיות מצוירת כהה כשהגדרת dark של המערכת מופעלת ושכל החלונות ברירת המחדל שלהם light; ערכי COLORREF של DWMWA_BORDER_COLOR, DWMWA_CAPTION_COLOR ו-DWMWA_TEXT_COLOR ושחזור ברירת המחדל עם DWMWA_COLOR_DEFAULT; תמיכה מ-Windows 11 build 22000; ותמיכה ב-DWMWA_SYSTEMBACKDROP_TYPE מ-build 22621.  2 3 4

  5. Microsoft Learn, UISettings.ColorValuesChanged Event. על האירוע שנורה כשערך צבע משתנה.  2

  6. Microsoft Learn, What’s new in Windows Forms for .NET 9. על תמיכה ראשונית ניסיונית ב-dark mode, SystemColors שמשתנה בהתאם כשמצב הצבע משתנה, שלושת ערכי SystemColorMode Classic, System ו-Dark, קריאה ל-Application.SetColorMode בקוד ההפעלה, ודיכוי WFO5001.  2 3

  7. Microsoft Learn, What’s new in Windows Forms for .NET 10. על השילוב המלא של dark mode ו-SetColorMode שכבר אינו ניסיוני, Win32 common controls בתוך owner-drawn controls שנשארים בהירים אלא אם עושים opt-in, והצורך לקרוא ל-SetStyle(ControlStyles.ApplyThemingImplicitly) לפני base.CreateParams בתוך override של CreateParams כי ה-constructor מאוחר מדי.  2 3 4

  8. Microsoft Learn, What’s new in WPF for .NET 9. על Fluent theme שתומך ב-light/dark ובצבע ה-accent, ארבעת ערכי ThemeMode Light, Dark, System ו-None והגדרה על Application או Window, החלה דרך resource dictionaries, והגדרת ThemeMode מקוד שהיא ניסיונית ודורשת דיכוי WPF0001.  2 3 4

  9. Microsoft Learn, What’s new in WPF for .NET 10. שתמיכת סגנון Fluent UI עדיין בעבודה, הסגנונות שנוספו ל-DatePicker, GridSplitter, GridView, GroupBox, Hyperlink, Label, NavigationWindow, RichTextBox ו-TextBox, ותיקוני הקריסה שקשורים ל-HighContrast.  2 3

  10. Microsoft Learn, Application.ThemeMode Property. שהוא שולט אם Fluent theme נטען במצב light, dark או system וגם שולט בהחלת backdrop material ו-dark mode על החלון, ש-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, השמטת תמונות bitmap מאחורי טקסט, וציור תמונות רב-צבעיות בצבעי foreground ו-background.  2 3

  12. Microsoft Learn, High-contrast mode. של-Aero יש טקסט שחור וצבע בחירה כחול בהיר אבל ל-High Contrast Black יש צבע בחירה שחור, מה שיכול לייצר טקסט שחור על שחור; ש-COLOR_HIGHLIGHTTEXT מתוכנן להיות מזווג עם COLOR_HIGHLIGHT ו-COLOR_WINDOWTEXT עם COLOR_WINDOW; לא לקבע צבעי טקסט; לבנות UI שלא תלוי ב-theme כי משתמשים מתאימים אישית את הצבעים; לחשב מחדש צבעים ב-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 תרגום יפני. על Success Criterion 1.4.3 (Contrast (Minimum)) של טקסט 4.5:1 וטקסט גדול 3:1, ועל Success Criterion 1.4.1 (Use of Color).  2 3

  15. Microsoft Learn, Color in Windows. של-Windows יש שני מצבי צבע, light ו-dark, שבחירת accent color ו-theme משתקפת בחוויית המשתמש כולה, ועל הבטחת contrast והתחשבות בגיוון ראיית צבע.  2

  16. Microsoft Learn, Reference for Windows 11 and Windows 10 settings. ש-AppsUseLightTheme ו-SystemUsesLightTheme תחת HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize הם ערכי DWORD שמייצגים את מצב light/dark של אפליקציות ושל Windows. 

  17. Microsoft Learn, Theming in Windows apps. שהסרת RequestedTheme גורמת למעקב אחרי הגדרת המערכת, שבחירת high contrast theme על ידי המשתמש גורמת למערכת לדרוס את RequestedTheme, ושבתבניות מותאמות לא מקבעים צבעים אלא משתמשים ב-theme brushes. 

  18. Microsoft Learn, DwmSetWindowAttribute function (dwmapi.h). על הפונקציה שמגדירה את מאפייני הציור של DWM לאזור ה-non-client של חלון, ועל הזמינות מ-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 ו-DWMSBT_TRANSIENTWINDOW ל-Acrylic ב-Windows 11, שהאפקט של החומר עשוי להשתנות בגרסאות עתידיות של Windows, ותמיכה מ-Windows 11 build 22621. 

  21. Microsoft Learn, UISettings.GetColorValue(UIColorType) Method. על המתודה שמחזירה את ערך הצבע של UIColorType שצוין. 

  22. Microsoft Learn, WM_SETTINGCHANGE message. על ההודעה שנשלחת לכל החלונות ברמה העליונה כש-SystemParametersInfo משנה הגדרה כלל-מערכתית או כשהגדרת policy משתנה. 

  23. Microsoft Learn, SystemEvents.UserPreferenceChanged Event. על האירוע הסטטי שנורה כשהעדפת משתמש משתנה, ועל דליפת הזיכרון שנובעת מאי-ניתוק ה-handler. 

  24. Microsoft Learn, WM_THEMECHANGED message. שהוא משודר לכל החלונות אחרי ש-theme מופעל, מבוטל או מוחלף, וש-theme handles קיימים נהיים לא תקפים וחייבים להיפתח מחדש. 

  25. Microsoft Learn, WM_SYSCOLORCHANGE message. שהוא נשלח לכל החלונות ברמה העליונה כשהגדרת צבע מערכת משתנה, שמברשות שמשתמשות בצבעי מערכת חייבות להיווצר מחדש, ושהוא חייב להיות מועבר ל-common controls. 

  26. Microsoft Learn, Compiler Error WFO5001. ש-SetColorMode ו-SystemColorMode היו מוגנים כיכולות ניסיוניות להערכה ב-.NET 9, ושהשגיאה לא חלה מ-.NET 10. 

  27. Microsoft Learn, SystemColors.UseAlternativeColorSet Property. שהגדרה ל-true גורמת לערכי KnownColor של המערכת להחזיר סט צבעים חלופי (כרגע גרסת dark mode), שזו יכולת ניסיונית תחת SYSLIB5002, ושערכי KnownColor של המערכת תמיד מחזירים את צבעי Windows הנוכחיים כש-high contrast theme פעיל ב-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 וקבלת שינויי high contrast באירוע Changed, ועל כך ששחרור ההפניה משמיד את האובייקט והאירועים מפסיקים. 

  32. Microsoft Learn, SystemColors.WindowBrushKey Property. שהפניה דינמית עם מפתח משאב מתעדכנת אוטומטית כשהמברשת משתנה, והפניה סטטית עם WindowBrush לא מתעדכנת אוטומטית. 

  33. Microsoft Learn, ResourceDictionary.ThemeDictionaries Property (Microsoft.UI.Xaml). שפקד מותאם עם מילוני Light ו-Dark צריך גם מילון HighContrast, ש-HighContrast הוא מפתח ה-fallback כשאין theme high-contrast אחר, ש-Default משמש כש-ResourceDictionary של theme לא נמצא, ושמשאבי צבע מערכת כמו SystemColorButtonFaceColor ניתנים לשימוש ב-HighContrast. 

  34. Microsoft Learn, Windows app development best practices. ש-Windows 11 עבר מלבן טהור ושחור טהור לגוונים שקלים יותר לעיניים, וש-dark/light themes הם אמצעי התאמה להעדפה החזותית של המשתמש. 

  35. Microsoft Learn, Color (Windows UX guidelines). על שימוש בצבע כחיזוק חזותי ולא כאמצעי התקשורת העיקרי, בחירת צבעי theme וצבעי מערכת לפי מטרה ושימוש ב-foreground ו-background בצירופים תואמים, טיפול בשינוי theme ב-WM_THEMECHANGED, וש-High Contrast Black ב-Windows 11 תואם ל-Aquatic ו-High Contrast White ל-Desert. 

מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

המאמר קשור ישירות לשירותים הבאים.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

העברתי את Windows ל-dark mode, אבל ה-title bar של אפליקציית WinForms הפנימית שלנו נשאר לבן. למה?
כי ל-Windows אין דרך לדעת אם אפליקציה תומכת ב-dark mode, ולכן לתאימות היא מתייחסת לכל חלון כ-light mode כברירת מחדל. אזור ה-non-client, כולל ה-title bar, מצויר על ידי Desktop Window Manager (DWM), והמסגרת מצוירת כהה כשהמערכת כהה רק אחרי שהאפליקציה מעבירה TRUE ל-DWMWA_USE_IMMERSIVE_DARK_MODE (ערך 20) דרך DwmSetWindowAttribute. התמיכה במאפיין הזה מתועדת ל-Windows 11 build 22000 ואילך. אם משתמשים ב-Application.SetColorMode ב-WinForms ב-.NET 9 ואילך, או ב-ThemeMode ב-WPF ב-.NET 9 ואילך, ה-framework קורא לזה בשבילכם, כך שקריאה עצמית נחוצה רק באפליקציות ב-.NET 8 ומטה, ב-.NET Framework, או ב-Win32/MFC. שימו לב ש-title bar כהה מעל client area שנשאר לבן נראה גרוע יותר, לא טוב יותר. מפעילים את המאפיין הזה רק כשמוכנים לצבוע מחדש את כל האפליקציה בכהה.
האם dark mode ו-contrast themes (high contrast) הם אותו דבר?
לא. Light/dark הוא מצב הצבע תחת Settings > Personalization > Colors, והוא משתמש בלוח רחב שמחליף את הבהירות של foreground ו-background. Contrast theme נבחר תחת Settings > Accessibility > Contrast themes ומשתמש בלוח מוגבל עם יחס contrast של בערך 7:1 ומעלה (ארבעת ה-themes המובנים Aquatic, Desert, Dusk ו-Night sky, ועוד כל צבע שהמשתמש ערך). התיעוד של Microsoft אומר במפורש לא לבלבל בין השניים, ו-dark mode אינו זמין כל עוד contrast theme פעיל (SetColorMode ב-WinForms אינו מספק dark mode במהלך contrast theme, ו-RequestedTheme ב-XAML נדרס על ידי המערכת). במימוש, בודקים קודם אם contrast theme פעיל, ואם כן משאירים הכל לצבעי המערכת; רק אחרת בוחרים את לוח light או dark. זה סדר העדיפות.
מה הדרך הקצרה ביותר לגרום לאפליקציית WinForms לתמוך ב-dark mode?
ב-.NET 9 ואילך, הדרך הקצרה היא לקרוא ל-Application.SetColorMode(SystemColorMode.System) לפני Application.Run ב-Program.cs. ב-.NET 9 זו הייתה יכולת ניסיונית, ולכן היה צריך לדכא WFO5001 בקובץ הפרויקט; מ-.NET 10 זה עובד בלי דיכוי. קריאה ל-SetColorMode מחליפה את SystemColors לסט חלופי ל-dark mode, והפקדים הסטנדרטיים מצוירים בהתאם. יש שלוש הסתייגויות. ראשית, dark mode זמין רק ב-Windows 11 ואילך ומבוטל כל עוד contrast theme פעיל. שנית, גם עם SystemColorMode.System האפליקציה לא עוקבת אחרי שינוי בהגדרת Windows בזמן שהיא רצה (השינוי נכנס בהפעלה הבאה). שלישית, אם owner-drawn control משתמש ב-Win32 common controls כמו פסי גלילה, צריך לעקוף את CreateParams ולקרוא ל-SetStyle(ControlStyles.ApplyThemingImplicitly, true) לפני base.CreateParams (ה-constructor מאוחר מדי).
מה אפליקציית WPF צריכה כדי לעקוב אחרי dark mode?
WPF ב-.NET 9 ואילך מגיע עם theme חדש שעוקב אחרי Fluent design של Windows 11, וכתיבת ThemeMode="System" על אלמנט Application ב-App.xaml מספיקה כדי לטעון את Fluent theme שתואם להגדרת light/dark של Windows. ThemeMode גם שולט בהכהיית החלון (ה-title bar) ובהחלת backdrop material. עם זאת, קריאה וכתיבה של מאפיין ThemeMode מקוד עדיין ניסיוניות ב-.NET 10 (WPF0001), וסגנונות Fluent עצמם מתוארים כ-"עדיין בעבודה" בתיעוד של .NET 10. לפני אימוץ באפליקציה עסקית, בודקים אם הפקדים שבהם משתמשים מצוירים נכון תחת Fluent. אם נשארים ב-theme הקלאסי (אותו דבר חל על .NET 8 ומטה ועל .NET Framework), מכינים ResourceDictionary ל-light ול-dark, מחליפים אותם ב-MergedDictionaries, מפנים אליהם מ-XAML עם DynamicResource, ומשתמשים ב-UISettings.ColorValuesChanged כדי לזהות את ההחלפה. ל-title bar, לוקחים את ה-HWND מ-WindowInteropHelper ב-SourceInitialized וקוראים ל-DwmSetWindowAttribute.
למה טקסט נעלם או נהיה בלתי קריא תחת contrast theme (high contrast)?
הסיבות הטיפוסיות הן צבעים hardcoded, או שבירת הזיווג של צבעי מערכת של foreground ו-background. תחת contrast theme המשתמש יכול לערוך בחופשיות את צבעי הרקע, הטקסט, הקישור ואחרים, כך שכל הנחה כמו "הטקסט יהיה שחור" או "שורת הבחירה תהיה כחול בהיר" מתפרקת. לדוגמה, אם רק הרקע מקובע ל-#E6E6E6, חלק מה-themes נותנים foreground לבן, וטקסט לבן על אפור בהיר נהיה בלתי קריא. יש שלושה עקרונות. מזהים את המצב עם SPI_GETHIGHCONTRAST (SystemInformation.HighContrast ב-WinForms, SystemParameters.HighContrast ב-WPF); מחליפים כל צבע בזוג system-color הנכון (WindowText עם Window, ButtonText עם ButtonFace, HighlightText עם Highlight); ומורידים תמונות מאחורי טקסט וגרפיקה רב-צבעית, ומציירים רק בצבעי foreground ו-background. אסור להשתמש ב-GrayText לשום דבר מלבד מצב disabled, וב-Hotlight לשום דבר מלבד hyperlinks. שינויים מוכרזים ב-WM_SYSCOLORCHANGE ו-WM_THEMECHANGED (SystemEvents.UserPreferenceChanged ב-.NET), ולכן מחשבים שם מחדש את הצבעים ומציירים מחדש.

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג