Dark mode ו-Contrast themes באפליקציות Windows — title bar כהה של DWM, מעקב theme ב-WinForms/WPF, וציור תחת High Contrast
· עודכן בתאריך: · Go Komura · 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 חלק מהדברים מממשים ידנית). רמת הקושי היא בינונית.
flowchart TB
accTitle: הזרימה של המאמר הזה
accDescr: מבנה המאמר, שמחבר לפי הסדר את שני צירי ה-theme, למה חלונות ברירת המחדל היא light, title bar כהה של DWM, מנגנון הזיהוי והמעקב, המימושים ב-WinForms וב-WPF, ציור תחת contrast theme, בחירת מדיניות, ואימות
axes["מסדרים את שני צירי ה-theme"] --> why["למה ברירת המחדל היא light"]
why --> dwm["title bar כהה של DWM"]
dwm --> detect["זיהוי ומעקב"]
detect --> impl["מימוש WinForms/WPF"]
impl --> hc["ציור תחת contrast theme"]
hc --> policy["בחירת מדיניות ואימות"]
איור 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.
flowchart TB
accTitle: שני צירי ה-theme וסדר העדיפות
accDescr: סדר ההחלטה שבו האפליקציה משאירה הכל לזוגות צבעי המערכת אם contrast theme פעיל, ואחרת קוראת את מצב הצבע light/dark ובוחרת את לוח האפליקציה
q1{"Contrast theme פעיל?"}
q1 -->|כן| sys["משאירים לזוגות צבעי המערכת"]
q1 -->|לא| q2{"מצב הצבע?"}
q2 -->|Light| light["לוח light"]
q2 -->|Dark| dark["לוח dark"]
sys -.-> note["Dark 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
flowchart TB
accTitle: שני אזורי החלון ומי מצייר אותם
accDescr: אזור ה-non-client שעשוי מ-title bar וממסגרת מצויר על ידי DWM, ואזור ה-client מצויר על ידי האפליקציה או UI framework, ולכן תמיכה ב-dark mode נחוצה בשניהם
win["חלון ברמה העליונה"] --> nc["אזור non-client (title bar, מסגרת)"]
win --> client["אזור client (תוכן החלון)"]
nc --> dwm["מורכב ומצויר על ידי DWM"]
client --> app["מצויר על ידי האפליקציה או ה-framework"]
dwm -.-> attr["מונחה דרך DwmSetWindowAttribute"]
app -.-> palette["לוח הצבעים של האפליקציה עצמה"]
איור 3: DWM מצייר את ה-title bar והאפליקציה מציירת את התוכן, כך שתמיכה ב-dark mode צריכה גם הנחיה ל-DWM וגם את לוח האפליקציה.
שתי מסקנות נובעות מזה. ראשית, כדי להפוך את ה-title bar לכהה, האפליקציה חייבת לבקש במפורש מ-DWM. שנית, מה שנהיה כהה כתוצאה מהבקשה הוא רק ה-title bar; אזור ה-client חייב להיות מצויר מחדש על ידי האפליקציה עצמה. התיעוד גם אומר ש-“כדי לתמוך ב-dark mode במלואו, כל משטח האפליקציה צריך לעקוב אחרי ה-dark theme”, ומציין שהמדריך הרשמי מכסה רק זיהוי ו-title bar, לא איך לצייר מחדש את אזור ה-client.3 אפליקציה עם title bar שחור ותוכן לבן נראית פחות טבעית מאחת שנשארת לבנה לכל אורכה.
flowchart TB
accTitle: איך ברירת המחדל נהיית light
accDescr: Windows לא יודע אם אפליקציה תומכת ב-dark mode, ולכן לתאימות ברירת המחדל היא light, ורק כשהאפליקציה מעבירה TRUE דרך מאפיין DWM הוא מצייר את המסגרת לפי הגדרת dark של המערכת
unknown["Windows לא יכול לדעת אם האפליקציה תומכת"] --> def["ברירת המחדל היא light, לתאימות"]
def --> q{"האם האפליקציה העבירה TRUE?"}
q -->|לא| light["תמיד מסגרת בהירה"]
q -->|כן| follow["מצויר לפי הגדרת המערכת"]
איור 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.
flowchart TB
accTitle: מתי לקרוא ל-DwmSetWindowAttribute
accDescr: מגדירים את מאפיין DWM אחרי ש-window handle נוצר, מגדירים אותו שוב על ה-handle החדש כשה-handle נוצר מחדש, ומגדירים אותו שוב כשמגיעה התראת שינוי theme
create["HWND נוצר"] --> apply["מגדירים את מאפיין DWM"]
apply --> run["מוצג"]
run -->|Handle נוצר מחדש| create
run -->|התראת שינוי theme| apply
איור 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)”, וצבע מותג הוא אפשרות כשבאמת צריך אותו.
flowchart TB
accTitle: איך מחליטים על צבע ה-title bar
accDescr: מעקב אחרי ברירת המחדל של המערכת דורש רק הגדרת ערך 20 ל-TRUE, אבל צביעת צבע מותג מעבירה לאפליקציה את האחריות להבטיח contrast של טקסט וכפתורי כיתוב ולנהל צבעים פעילים ולא-פעילים, ושחזור מעביר DWMWA_COLOR_DEFAULT
q{"צבע ה-title bar?"}
q -->|לעקוב אחרי ברירת המחדל של המערכת| dark["רק מגדירים ערך 20 ל-TRUE"]
q -->|לצבוע צבע מותג| brand["מציינים את מאפייני הצבע (34 עד 36)"]
brand --> resp["מבטיחים בעצמכם contrast של טקסט וכפתורים"]
brand --> states["מנהלים בעצמכם פעיל/לא-פעיל"]
brand -.-> reset["משחזרים עם COLOR_DEFAULT"]
איור 6: בחירת צבע מותג מעבירה לאפליקציה את האחריות ל-contrast ולניהול מצב, ולכן לרוב האפליקציות העסקיות מעקב אחרי ברירת המחדל הוא התשובה הנכונה.
5. זיהוי ומעקב אחרי ה-theme של המערכת — קוראים, מבחינים, מציירים מחדש
העבודה של לגרום לאזור ה-client לעקוב אחרי ה-theme מתפרקת לשלושה חלקים: קוראים את ההגדרה הנוכחית, מבחינים בשינויים, ו-מציירים מחדש.
flowchart TB
accTitle: שלושת הצעדים של זיהוי ומעקב
accDescr: הלולאה של קריאת מצב הצבע הנוכחי עם UISettings בהפעלה, הבחנה בשינויים דרך התראות כמו ColorValuesChanged, החזרה ל-UI thread, וציור מחדש של לוח האפליקציה
read["קוראים: UISettings.GetColorValue"] --> paint["מציירים מחדש: מחילים שוב את הלוח"]
notice["מבחינים: ColorValuesChanged"] --> ui["מחזירים ל-UI thread"]
ui --> read
paint -.-> dwm["מגדירים שוב גם את מאפיין 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 לאבחון.
flowchart TB
accTitle: דרכים לקרוא את מצב הצבע
accDescr: הנתיב הקנוני הוא לקחת את צבע ה-foreground דרך WinRT UISettings ולסווג אותו כ-light או dark; ערך Registry AppsUseLightTheme הוא מקום האחסון ויש לשמור אותו לאבחון
q["Light או dark עכשיו?"] --> uis["UISettings.GetColorValue"]
q -.-> reg["Registry AppsUseLightTheme"]
uis --> fg["שופטים את הבהירות הנתפסת של ה-foreground"]
fg --> ans["בהיר פירושו dark mode"]
reg -.-> diag["מקום אחסון. שומרים לאבחון"]
איור 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.
flowchart TB
accTitle: נתיבי התראת שינוי theme ו-threads
accDescr: ColorValuesChanged מ-UISettings עשוי להגיע מחוץ ל-UI thread ומוחזר, WM_SETTINGCHANGE מגיע כ-SystemEvents.UserPreferenceChanged, ו-WM_THEMECHANGED ו-WM_SYSCOLORCHANGE מגיעים ל-window procedure. כולם מתכנסים לאותה שגרת החלה מחדש
cvc["ColorValuesChanged"] --> marshal["מחזירים ל-UI thread"]
upc["UserPreferenceChanged"] --> reapply["קוראים ומציירים מחדש"]
wm["WM_THEMECHANGED וכו'"] --> reapply
marshal --> reapply
איור 9: יש כמה נתיבי התראה, אבל כולם מתכנסים לאותה שגרת “קוראים ומציירים מחדש”.
5.3. מציירים מחדש — אוספים צבעים במקום אחד
התנאי שמאפשר ציור מחדש הוא ש-צבעים נאספים במקום אחד. אם Color.White ו-#FFFFFF מפוזרים בטפסים וב-XAML, אי אפשר למנות את המקומות לצייר מחדש. ב-WinForms, יוצרים מחלקת “לוח” (שני מופעים, אחד light ואחד dark) והפקדים לוקחים ממנה צבעים בהפעלה ובכל התראה. ב-WPF, אוספים צבעים ב-ResourceDictionary ומפנים אליהם מ-XAML עם DynamicResource. ב-WinUI, המבנה הזה מסופק מההתחלה כ-ThemeDictionaries.
flowchart TB
accTitle: המבנה שאוסף צבעים במקום אחד
accDescr: האפליקציה מחזיקה לוח light אחד ולוח dark אחד, וכל מסך לוקח את הצבעים מהלוח שנבחר למצב הנוכחי, כך שאפשר למנות את יעדי הציור מחדש
mode["המצב הנוכחי"] --> sel{"איזה?"}
sel -->|Light| pl["לוח light"]
sel -->|Dark| pd["לוח dark"]
pl --> screens["כל מסך ופקד"]
pd --> screens
hard["Color.White מפוזר"] -.-> cannot["אי אפשר למנות מה לצייר מחדש"]
איור 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
- מצב הצבע dark זמין רק ב-Windows 11 ואילך.
- Dark mode אינו זמין כש-contrast theme פעיל.
- גם עם
SystemColorMode.System, האפליקציה לא עוקבת אוטומטית אחרי שינוי בהגדרת Windows בזמן שהיא רצה.
השלישית נוטה לייצר שאלות תמיכה באפליקציות עסקיות. כותבים בתיעוד למשתמש שהמצב נקבע לפי הגדרת Windows בהפעלה ונכנס בהפעלה הבאה. אם חייבים לעקוב אחרי החלפה בזמן ריצה, צריך תכנון שיוצר מחדש את הטפסים, וכמעט אף פעם זה לא שווה את זה.
flowchart TB
accTitle: הזרימה והמגבלות של SetColorMode
accDescr: SetColorMode נקרא לפני Application.Run, SystemColors עובר לסט החלופי, והפקדים הסטנדרטיים עוקבים. יש לו שלוש מגבלות: Windows 11 בלבד, מבוטל במהלך contrast theme, ואין מעקב אחרי שינויי הגדרה בזמן ריצה
sc["SetColorMode (System)"] --> before["לפני Application.Run"]
before --> colors["SystemColors עובר לסט החלופי"]
colors --> ctrls["הפקדים הסטנדרטיים עוקבים"]
sc -.-> c1["Windows 11 בלבד"]
sc -.-> c2["מבוטל במהלך contrast theme"]
sc -.-> c3["לא עוקב אחרי שינויים בזמן ריצה"]
איור 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;
}
}
}
flowchart TB
accTitle: מתי אפשר להגדיר ApplyThemingImplicitly
accDescr: ApplyThemingImplicitly נקבע בנקודה שבה constructor של מחלקת הבסיס קורא ל-CreateParams, כך שצריך לקרוא ל-SetStyle לפני base.CreateParams בתוך override של CreateParams, וה-constructor של המחלקה הנגזרת מאוחר מדי
basector["Constructor של מחלקת הבסיס"] --> cp["קורא ל-CreateParams"]
cp --> st["SetStyle חייב לקרות עד כאן"]
st --> derived["Constructor של המחלקה הנגזרת"]
derived -.-> late["קריאה כאן מאוחרת מדי"]
איור 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. יש שתי אפשרויות.
- מצהירים על light קבוע. משאירים את מאפיין DWM בברירת המחדל FALSE (תמיד title bar בהיר) ומשאירים את אזור ה-client כמו שהוא. תמיכה ב-contrast theme (פרק 8) עדיין חובה.
- מממשים תמיכה מלאה בעצמכם. מאחדים את הלוח, קוראים ועוקבים עם
UISettings, מגדירים את מאפיין DWM ל-TRUE, ומציירים מחדש הכל כולל המראה של ה-common controls.
אפשרות 2 נוטה להסתיים ב-“בעיקר כהה, אבל בהיר במקומות”, כי האפליקציה לא יכולה לשלוט במלואו בציור של Win32 common controls (פסי גלילה, headers, כפתורי הרחבת עץ, וכן הלאה). כפי שהמדריך הרשמי אומר, “כל המשטח צריך לעקוב”;3 dark mode חצי-גמור הוא חוויה גרועה יותר מ-light קבוע. לנכסים קיימים, בוחרים באפשרות 1, מצהירים ש-“האפליקציה הזו מציגה ב-light mode”, ועוברים ל-SetColorMode כשמגררים ל-.NET 10. זו המדיניות המציאותית, והקלה ביותר להסביר.
flowchart TB
accTitle: אפשרויות WinForms לפי runtime
accDescr: ב-.NET 10 ואילך משתמשים ב-SetColorMode, ב-.NET 9 באותו API עם WFO5001 מדוכא, וב-.NET 8 ומטה או ב-.NET Framework בוחרים בין הצהרה על light קבוע לבין מימוש תמיכה מלאה בעצמכם עד ה-common controls
q{"Runtime?"}
q -->|.NET 10 ואילך| n10["SetColorMode (System)"]
q -->|.NET 9| n9["SetColorMode + דיכוי WFO5001"]
q -->|.NET 8 ומטה / .NET Framework| legacy{"מה לעשות?"}
legacy -->|מומלץ| fixed["מצהירים על light קבוע"]
legacy -->|אם מוכנים לזה| full["תמיכה מלאה בעצמכם"]
full -.-> partial["ה-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
flowchart TB
accTitle: מה ThemeMode ב-WPF עושה
accDescr: הגדרת ThemeMode ל-System טוענת את מילוני Fluent theme שתואמים להגדרת Windows למשאבים וגם שולטת בהחלת dark mode ו-backdrop material על החלון
tm["ThemeMode=System"] --> read["קוראים את הגדרת Windows"]
read --> dict["טוענים את מילוני Fluent למשאבים"]
read --> win["מכהים את החלון ומחילים את ה-backdrop"]
dict --> sync["נשארים בסנכרון עם 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 בעצמכם הוא כדלקמן.
- מגדירים צבעים ומברשות עם אותם מפתחות ב-
Themes/Light.xamlל-light וב-Themes/Dark.xamlל-dark. - מפנים אליהם מ-XAML עם DynamicResource, כמו
{DynamicResource App.WindowBackgroundBrush}(StaticResourceמקובע בזמן טעינה ולא עוקב אחרי החלפה). - בהתראה מ-
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; // מחליפים במקום, באותה עמדה
}
}
}
flowchart TB
accTitle: החלפת resource dictionaries ב-WPF
accDescr: מגדירים צבעים עם אותם מפתחות במילון light ובמילון dark, מפנים אליהם מ-XAML עם DynamicResource, ומחליפים את המילון ב-MergedDictionaries בהתראת שינוי theme כך שההפניות מתעדכנות
notify["התראת שינוי theme"] --> swap["מחליפים את המילון ב-MergedDictionaries"]
light["Light.xaml (אותם מפתחות)"] --> swap
dark["Dark.xaml (אותם מפתחות)"] --> swap
swap --> dyn["הפניות DynamicResource מתעדכנות"]
static["הפניות StaticResource"] -.-> stale["שומרות את הערך מזמן הטעינה"]
איור 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
flowchart TB
accTitle: נתיבי זיהוי contrast theme
accDescr: Win32 SPI_GETHIGHCONTRAST הוא שיטת הזיהוי היחידה הנתמכת, ו-SystemInformation.HighContrast ב-WinForms, SystemParameters.HighContrast ב-WPF, ו-ThemeSettings.HighContrast ב-WinUI 3 מסופקים על ידי כל framework כעטיפות סביבו
spi["SPI_GETHIGHCONTRAST (שיטת הזיהוי היחידה)"] --> wf["WinForms SystemInformation"]
spi --> wpf["WPF SystemParameters"]
spi --> winui["WinUI 3 ThemeSettings"]
spi --> win32["Win32: קוראים ישירות"]
איור 16: שורש הזיהוי הוא Win32 API אחד, ולכל framework יש מאפיין שעוטף אותו.
8.2. עקרונות ציור — זוגות foreground ו-background
“High contrast parameter” של Microsoft מפרט שלושה דברים שאפליקציה צריכה לעשות כש-high contrast דלוק.11
- ממפים כל צבע לזוג אחד של צבעי foreground ו-background. משתמשים ב-
GetSysColorעם הזוגCOLOR_WINDOWTEXTו-COLOR_WINDOW, או הזוגCOLOR_BTNTEXTו-COLOR_BTNFACE. - מורידים תמונות bitmap שמוצגות מאחורי טקסט. הן מכשול חזותי למשתמשים שצריכים high contrast.
- מציירים תמונות רב-צבעיות בצבעי 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
flowchart TB
accTitle: איך שבירת הזוג הופכת טקסט לבלתי קריא
accDescr: הנחה שטקסט שחור ושימוש בצבע הבחירה של המערכת רק לרקע הבחירה נותנת שחור על שחור ב-High Contrast Black, שבו צבע הבחירה שחור. לקיחת foreground ו-background כזוג שומרת על טקסט קריא גם כשהמשתמש עורך את הצבעים
assume["מניחים שטקסט שחור"] --> hl["רק רקע הבחירה משתמש בצבע הבחירה של המערכת"]
hl --> black["ב-High Contrast Black צבע הבחירה שחור"]
black --> broken["טקסט שחור על שחור"]
pair["לוקחים foreground ו-background כזוג"] --> ok["קריא גם כשהמשתמש עורך את הצבעים"]
edit["המשתמש עורך את הצבעים"] -.-> assume
איור 17: שימוש בצבע מערכת בצד אחד בלבד יכול לתת שחור על שחור, אבל לקיחת הזוג נשארת קריאה גם כשהצבעים נערכים.
flowchart TB
accTitle: החלטות ציור תחת contrast theme
accDescr: כש-contrast theme פעיל, ממפים צבעים לזוגות צבעי המערכת, מורידים תמונות מאחורי טקסט, מציירים תמונות רב-צבעיות בשני צבעי foreground ו-background, ולא משתמשים בצבעים hardcoded
on["Contrast theme פעיל"] --> map["ממפים צבעים לזוגות"]
on --> img["מורידים תמונות מאחורי טקסט"]
on --> multi["מציירים גרפיקה רב-צבעית בשני צבעים"]
on --> nohard["בלי צבעים 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 שכוסה במאמר הקודם.
flowchart TB
accTitle: פיצול סכמת הצבע ב-WinForms
accDescr: ApplyColorScheme נקרא בהפעלה ובכל UserPreferenceChanged; אם SystemInformation.HighContrast הוא true משאירים לזוגות צבעי המערכת ומסירים את תמונת הרקע, ואם false לוקחים צבעים מלוח light/dark
start["הפעלה / UserPreferenceChanged"] --> apply["ApplyColorScheme"]
apply --> q{"SystemInformation.HighContrast?"}
q -->|True| sys["משאירים לזוגות SystemColors"]
sys --> noimg["מסירים את תמונת הרקע"]
q -->|False| pal["לוקחים צבעים מלוח 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>
flowchart TB
accTitle: הפניות צבע מערכת ב-WPF וה-trigger של contrast theme
accDescr: הפניה למפתחות משאב של SystemColors דרך DynamicResource עוקבת אחרי שינויי מברשת אוטומטית, ו-trigger שקשור ל-IsHighContrast על proxy שנרשם ל-StaticPropertyChanged מגיב להחלפה בזמן ריצה ועובר לצבעים ששומרים על הזוג. הפניה ישירה ל-WindowBrush לא מתעדכנת
key["מפנים ל-WindowBrushKey דרך DynamicResource"] --> auto["עוקב אחרי שינויי מברשת אוטומטית"]
spc["StaticPropertyChanged"] --> proxy["IsHighContrast על ה-proxy"]
proxy --> trig["DataTrigger מגיב"]
trig --> pair["עוברים לצבעים ששומרים על הזוג"]
direct["מפנים ל-WindowBrush ישירות"] -.-> stale["לא מתעדכן"]
איור 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
flowchart TB
accTitle: איך WinUI פותר ThemeDictionaries
accDescr: לפי ה-theme הנוכחי, אחד מהמילונים Default (dark), Light ו-HighContrast נבחר, ותחת HighContrast מפנים לצבעי מערכת דינמיים דרך ThemeResource. HighContrast הוא מפתח ה-fallback כשאין theme high-contrast בשם
theme{"ה-theme הנוכחי?"}
theme -->|Dark| def["מילון Default"]
theme -->|Light| light["מילון Light"]
theme -->|Contrast theme| hc["מילון HighContrast"]
hc --> sysc["מפנים למשאבי SystemColor דרך ThemeResource"]
hc -.-> fb["Fallback כשאין 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.
flowchart TB
accTitle: הקשר בין dark mode לנגישות
accDescr: תמיכה ב-dark mode היא עניין של העדפה חזותית וסביבה, בעוד שדרישות הנגישות של יחס contrast, העברת מידע ביותר מצבע בלבד, ותמיכה ב-contrast theme חייבות להתקיים בנפרד בלי קשר ל-theme
dark["תמיכה ב-dark mode"] --> pref["העדפה וסביבה"]
a11y["נגישות"] --> ratio["יחס contrast 4.5:1"]
a11y --> sole["לא צבע בלבד"]
a11y --> ct["תמיכה ב-contrast theme"]
dark -.->|לא תחליף ל| a11y
איור 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 מחוץ להיקף שלו |
flowchart TB
accTitle: הנתיב מנכסים קיימים לתמיכה ב-dark mode
accDescr: נכסים קיימים קודם מצהירים על light קבוע, משלימים תמיכה ב-contrast theme בלי לוותר, מאחדים את הלוח כהכנה, ואז מגררים ל-.NET 10 ועוברים ל-SetColorMode או ThemeMode. Dark mode חצי-גמור הוא חוויה גרועה יותר מ-light קבוע, ולכן הנתיב הזה לא נלקח
now["מצהירים על light קבוע (עכשיו)"] --> ct["תמיכה ב-contrast theme (חובה)"]
ct --> pal["מאחדים את הלוח (הכנה)"]
pal --> mig["מגררים ל-.NET 10"]
mig --> dark["עוברים ל-SetColorMode / ThemeMode"]
now -.->|לא נלקח| half["Dark mode חצי-גמור"]
half -.-> worse["חוויה גרועה יותר מ-light קבוע"]
איור 23: נכסים קיימים מתחילים מ-light קבוע, עוברים דרך תמיכה ב-contrast theme ואיחוד לוח, ועוברים לתמיכה ב-dark mode כשמגררים ל-.NET 10.
“Light קבוע” אינו הפסד. זו התנהגות ברירת המחדל של Windows עצמה, והתנהגות מתועדת. אפליקציה שעקבית ב-light טובה בהרבה למשתמשים מאשר dark mode חצי-גמור שמגיע עם “רק ה-title bar שחור” או “רק פסי הגלילה לבנים”. עם זאת, תמיכה ב-contrast theme היא הדבר האחד שאי אפשר “לקבע”. היא נופלת תחת ההתאמה הסבירה שכוסתה במאמר הקודם: זה עניין של האם האפליקציה שמישה, לא עניין של העדפת theme.
11. רשימת בדיקה לאימות
אחרי שהעבודה נעשתה, מאמתים על מכונה אמיתית בסדר הבא. כל צעד אפשר להחליף מאפליקציית Settings בשניות.
- מחליפים light/dark בזמן שהאפליקציה רצה. משנים את המצב תחת Settings > Personalization > Colors, ומאשרים שגם ה-title bar וגם אזור ה-client עוקבים, או שהאפליקציה מתנהגת כמפורט עם “נכנס בהפעלה הבאה” (
SetColorModeב-WinForms לא עוקב). - מפעילים יצירת handle מחדש. ב-WinForms, מחליפים
ShowInTaskbarבזמן ריצה ומאשרים שמאפיין ה-title bar נשמר. - מנסים את כל ארבעת contrast themes. מחליפים עם left Alt + left Shift + Print Screen, ומאשרים בכל אחד מ-Aquatic, Desert, Dusk ו-Night sky שטקסט, גבולות, שורות נבחרות, פריטים מבוטלים וקישורים קריאים.1
- עורכים את צבעי contrast theme. משתמשים באמת משנים את הצבעים. עורכים את הרקע לצבע קיצוני כדי לשטוף החוצה צבעים hardcoded שנשארו.
- בודקים את הלוגים. מאשרים שכשל של
DwmSetWindowAttributeאוSystemParametersInfoב-OS נתמך נרשם, ושב-Windows 10 מתחת ל-build 22000 האפליקציה עולה ב-light בלי לקרוא למאפיין DWM. - מודדים את יחסי ה-contrast. גם ב-light וגם ב-dark, בודקים כל צירוף של צבע טקסט ורקע בלוח מול 4.5:1.14
flowchart TB
accTitle: צעדי אימות לתמיכה ב-theme
accDescr: מאמתים על מכונה אמיתית בסדר הזה: החלפת light/dark בזמן ריצה, יצירת handle מחדש, ארבעת contrast themes, עריכת צבעי ה-theme, בדיקת לוגי כשל, ומדידת יחסי contrast
s1["מחליפים light/dark בזמן ריצה"] --> s2["יצירת handle מחדש"]
s2 --> s3["ארבעת contrast themes"]
s3 --> s4["עורכים את צבעי ה-theme"]
s4 --> s5["בודקים את לוגי הכשל"]
s5 --> s6["מודדים את יחסי ה-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 פעיל, ולכן מנסים את השניים בנפרד). תוך כמה דקות תראו “איפה האפליקציה שלכם מחזיקה את הצבעים”.
מאמרים קשורים
- מבוא לנגישות באפליקציות Windows — מתכוננים ל-UI Automation ולחובת התאמה סבירה
- WinRT הוא COM — IInspectable, .winmd, language projections, ולמה WinUI עדיין יושב על חוזה בינארי
- קריאה בטוחה ל-Win32 APIs מ-C# — מדריך P/Invoke מעשי (DllImport / LibraryImport / CsWin32)
- UI thread ו-async/await ב-WPF/WinForms, בדף אחד
- תמיכה ב-High-DPI ב-WinForms — למה ה-UI מטשטש או נשבר במסכי 4K, ותיקונים מעשיים
- תמיכה ב-High-DPI ב-WPF — למה זה עדיין מטשטש ונמרח למרות ש”אמור להיות DPI-aware”, ואיך מתקנים
- בחירה בין WinForms, WPF ו-WinUI — טבלת החלטה מעשית
תחומי ייעוץ קשורים
KomuraSoft LLC מטפלת בתמיכה ב-dark mode לאפליקציות עסקיות WinForms/WPF (איחוד הלוח, הערכת מיגרציה ל-SetColorMode / ThemeMode ב-.NET 9/10, שילוב מאפיין DWM), באבחון ותיקון שבירת תצוגה תחת contrast themes, ובייעוץ על מעקב theme בנכסי Win32/MFC. להתחיל מהשלב של “עובדים התלוננו ברגע שעברו ל-dark mode” זה בסדר.
קישורים
-
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
-
Microsoft Learn, Application.SetColorMode(SystemColorMode) Method. על קריאה לפני יצירת אלמנטי UI, שהאפליקציה לא מסתגלת אוטומטית כשהגדרת המערכת משתנה גם עם System, ושמצב הצבע dark זמין רק ב-Windows 11 ואילך ואינו זמין במצב high contrast. ↩ ↩2 ↩3 ↩4
-
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
-
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
-
Microsoft Learn, UISettings.ColorValuesChanged Event. על האירוע שנורה כשערך צבע משתנה. ↩ ↩2
-
Microsoft Learn, What’s new in Windows Forms for .NET 9. על תמיכה ראשונית ניסיונית ב-dark mode, SystemColors שמשתנה בהתאם כשמצב הצבע משתנה, שלושת ערכי SystemColorMode Classic, System ו-Dark, קריאה ל-Application.SetColorMode בקוד ההפעלה, ודיכוי WFO5001. ↩ ↩2 ↩3
-
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
-
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
-
Microsoft Learn, What’s new in WPF for .NET 10. שתמיכת סגנון Fluent UI עדיין בעבודה, הסגנונות שנוספו ל-DatePicker, GridSplitter, GridView, GroupBox, Hyperlink, Label, NavigationWindow, RichTextBox ו-TextBox, ותיקוני הקריסה שקשורים ל-HighContrast. ↩ ↩2 ↩3
-
Microsoft Learn, Application.ThemeMode Property. שהוא שולט אם Fluent theme נטען במצב light, dark או system וגם שולט בהחלת backdrop material ו-dark mode על החלון, ש-ThemeMode ו-Resources מתוכננים להישאר בסנכרון כדי להימנע מחוסר עקביות, ושהוא נושא את המאפיין Experimental(“WPF0001”) ועשוי להיות מוסר בעתיד. ↩ ↩2 ↩3 ↩4
-
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
-
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
-
Microsoft Learn, Walkthrough: Creating an Accessible Windows-based Application. על זיהוי עם SystemInformation.HighContrast; שימוש בסכמת הצבע של המערכת כשהוא דלוק, הוספת רמזים חזותיים למידע שמועבר בצבע, והשמטת תמונות מאחורי טקסט; בדיקה בהפעלה ומעקב אחרי אירוע UserPreferenceChanged; ודוגמת החלפת צבעי תווית עם SystemColors. ↩ ↩2 ↩3
-
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
-
Microsoft Learn, Color in Windows. של-Windows יש שני מצבי צבע, light ו-dark, שבחירת accent color ו-theme משתקפת בחוויית המשתמש כולה, ועל הבטחת contrast והתחשבות בגיוון ראיית צבע. ↩ ↩2
-
Microsoft Learn, Reference for Windows 11 and Windows 10 settings. ש-AppsUseLightTheme ו-SystemUsesLightTheme תחת HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize הם ערכי DWORD שמייצגים את מצב light/dark של אפליקציות ושל Windows. ↩
-
Microsoft Learn, Theming in Windows apps. שהסרת RequestedTheme גורמת למעקב אחרי הגדרת המערכת, שבחירת high contrast theme על ידי המשתמש גורמת למערכת לדרוס את RequestedTheme, ושבתבניות מותאמות לא מקבעים צבעים אלא משתמשים ב-theme brushes. ↩
-
Microsoft Learn, DwmSetWindowAttribute function (dwmapi.h). על הפונקציה שמגדירה את מאפייני הציור של DWM לאזור ה-non-client של חלון, ועל הזמינות מ-Windows Vista. ↩
-
Microsoft Learn, Retrieve a window handle (HWND). על לקיחת Handle מ-WindowInteropHelper ב-WPF. ↩
-
Microsoft Learn, DWM_SYSTEMBACKDROP_TYPE enumeration (dwmapi.h). ש-DWMSBT_MAINWINDOW תואם ל-Mica ו-DWMSBT_TRANSIENTWINDOW ל-Acrylic ב-Windows 11, שהאפקט של החומר עשוי להשתנות בגרסאות עתידיות של Windows, ותמיכה מ-Windows 11 build 22621. ↩
-
Microsoft Learn, UISettings.GetColorValue(UIColorType) Method. על המתודה שמחזירה את ערך הצבע של UIColorType שצוין. ↩
-
Microsoft Learn, WM_SETTINGCHANGE message. על ההודעה שנשלחת לכל החלונות ברמה העליונה כש-SystemParametersInfo משנה הגדרה כלל-מערכתית או כשהגדרת policy משתנה. ↩
-
Microsoft Learn, SystemEvents.UserPreferenceChanged Event. על האירוע הסטטי שנורה כשהעדפת משתמש משתנה, ועל דליפת הזיכרון שנובעת מאי-ניתוק ה-handler. ↩
-
Microsoft Learn, WM_THEMECHANGED message. שהוא משודר לכל החלונות אחרי ש-theme מופעל, מבוטל או מוחלף, וש-theme handles קיימים נהיים לא תקפים וחייבים להיפתח מחדש. ↩
-
Microsoft Learn, WM_SYSCOLORCHANGE message. שהוא נשלח לכל החלונות ברמה העליונה כשהגדרת צבע מערכת משתנה, שמברשות שמשתמשות בצבעי מערכת חייבות להיווצר מחדש, ושהוא חייב להיות מועבר ל-common controls. ↩
-
Microsoft Learn, Compiler Error WFO5001. ש-SetColorMode ו-SystemColorMode היו מוגנים כיכולות ניסיוניות להערכה ב-.NET 9, ושהשגיאה לא חלה מ-.NET 10. ↩
-
Microsoft Learn, SystemColors.UseAlternativeColorSet Property. שהגדרה ל-true גורמת לערכי KnownColor של המערכת להחזיר סט צבעים חלופי (כרגע גרסת dark mode), שזו יכולת ניסיונית תחת SYSLIB5002, ושערכי KnownColor של המערכת תמיד מחזירים את צבעי Windows הנוכחיים כש-high contrast theme פעיל ב-Windows. ↩
-
Microsoft Learn, HIGHCONTRASTW structure (winuser.h). על HCF_HIGHCONTRASTON (0x00000001) ב-dwFlags, והצורך לציין cbSize כשמשתמשים בו עם SPI_GETHIGHCONTRAST. ↩
-
Microsoft Learn, SystemParameters.HighContrast Property. על המאפיין הסטטי של WPF שממופה ל-SPI_GETHIGHCONTRAST ול-HCF_HIGHCONTRASTON. ↩
-
Microsoft Learn, SystemParameters.StaticPropertyChanged Event. על האירוע הסטטי שנורה כשמאפיין כלשהו של SystemParameters משתנה. ↩ ↩2
-
Microsoft Learn, ThemeSettings Class (Microsoft.UI.System). על יצירה קשורה לחלון עם CreateForWindowId וקבלת שינויי high contrast באירוע Changed, ועל כך ששחרור ההפניה משמיד את האובייקט והאירועים מפסיקים. ↩
-
Microsoft Learn, SystemColors.WindowBrushKey Property. שהפניה דינמית עם מפתח משאב מתעדכנת אוטומטית כשהמברשת משתנה, והפניה סטטית עם WindowBrush לא מתעדכנת אוטומטית. ↩
-
Microsoft Learn, ResourceDictionary.ThemeDictionaries Property (Microsoft.UI.Xaml). שפקד מותאם עם מילוני Light ו-Dark צריך גם מילון HighContrast, ש-HighContrast הוא מפתח ה-fallback כשאין theme high-contrast אחר, ש-Default משמש כש-ResourceDictionary של theme לא נמצא, ושמשאבי צבע מערכת כמו SystemColorButtonFaceColor ניתנים לשימוש ב-HighContrast. ↩
-
Microsoft Learn, Windows app development best practices. ש-Windows 11 עבר מלבן טהור ושחור טהור לגוונים שקלים יותר לעיניים, וש-dark/light themes הם אמצעי התאמה להעדפה החזותית של המשתמש. ↩
-
Microsoft Learn, Color (Windows UX guidelines). על שימוש בצבע כחיזוק חזותי ולא כאמצעי התקשורת העיקרי, בחירת צבעי theme וצבעי מערכת לפי מטרה ושימוש ב-foreground ו-background בצירופים תואמים, טיפול בשינוי theme ב-WM_THEMECHANGED, וש-High Contrast Black ב-Windows 11 תואם ל-Aquatic ו-High Contrast White ל-Desert. ↩
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
סיום התמיכה ב-printer drivers של Windows — איך אפליקציות עסקיות מתכוננות להדפסת דוחות ותוויות
Microsoft מפסיקה בהדרגה printer drivers מסוג v3/v4. מה Windows protected print mode מסיר, ואיך עושים inventory ומכינים הדפסת דוחות ותוויו...
יסודות נגישות באפליקציות Windows — UI Automation והכנה לחובת reasonable accommodation
איך screen readers קוראים אפליקציות Windows דרך UI Automation: מתן שמות ב-WinForms/WPF, מקלדת, ניגודיות וכלי בדיקה, על רקע תיקון חוק ביטו...
מה Not Responding באמת אומר — איך Windows מחליט שיש hang, ואיך לתכנן אפליקציות שלא נתקעות
Windows מסמן window כ-Not Responding אחרי 5 שניות בלי שליפת message ומציג ghost window: השיפוט, סיבות ה-hang, תכנון ה-UI thread, והחקירה.
איך clipboard ו-drag-and-drop עובדים — טיפול נכון ב-OLE data transfer באפליקציות עסקיות
למה הדבקת Excel מתפרקת ולמה paste נכשל אחרי סגירת המקור: clipboard formats, delayed rendering, OLE drag-and-drop, ומדיניות clipboard hist...
אותו 1 GB, ובכל זאת תיקיית תמונות מועתקת לאט יותר מסרטון אחד — למה?
למה נתונים באותו גודל מועתקים במהירויות שונות ב-Windows: מספר קבצים, latency של SSD ו-NAS, איגוד ב-ZIP, השוואת יצירה-העברה-חילוץ, ו-roboc...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
תהליכון ה-UI וטיימרים
תהליכון ה-UI של WPF / WinForms, זרימות אסינכרוניות, Dispatcher ותכנון טיימרים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
יישומים עסקיים, חיבור התקנים וכלי תקשורת, מהגדרת הדרישות ועד הפיתוח.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- העברתי את 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), ולכן מחשבים שם מחדש את הצבעים ומציירים מחדש.