امروز میخواستم برای یکی از پروژه هام قابلیتی رو پیاده سازی کنم که هماهنگ با تم ویندوز، تم برنامه رو عوض کنه (تیره/روشن). ینی وقتی تم ویندوز Dark میشه تم برنامه من هم Dark بشه و برعکس.
ساده ترین کار اینه که ما بیایم از کدهای WinRT که توسط بسته ناگت SDK Contract ارائه میشه استفاده کنیم. در این صورت کافیه فقط از کلاس ThemeManager استفاده کنیم و بدون کوچکترین خونریزی برناممون رو به این ویژگی مجهز کنیم? اما خب هرچیزی هزینه خودش رو داره و من به شخصه خوشم نمیاد 25 مگابایت فقط برای شناسایی وضعیت تم ویندوز خرج کنم! پس خودم دست به کار شدم تا یه Listener توپ بنویسم.
در دات نت یسری ایونت هست که مربوط به سیستم عامل میشن و از کلاس SystemEvents قابل دسترسی هست اینجا یه ایونت داریم به اسم UserPreferenceChanged که شامل مواردی میشه که کاربر تنظیمات ویندوز رو تغییر میده! هر تغییری که تو تنظیمات ویندوز اعمال بشه درون یکی از Category ها صدا زده میشه. پس اگر ما این ایونت رو رجیستر کنیم هر موقع تغییری تو تنظیمات ویندوز اعمال بشه برنامه ما متوجه میشه! پس این شدقدم اول:
یه کلاس درست میکنیم و توی متد سازنده این ایونت رو رجیستر میکنیم:
public ThemeHelper() { SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged; } private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e) { switch (e.Category) { case UserPreferenceCategory.Accessibility: break; case UserPreferenceCategory.Color: break; case UserPreferenceCategory.Desktop: break; case UserPreferenceCategory.General: break; case UserPreferenceCategory.Icon: break; case UserPreferenceCategory.Keyboard: break; case UserPreferenceCategory.Menu: break; case UserPreferenceCategory.Mouse: break; case UserPreferenceCategory.Policy: break; case UserPreferenceCategory.Power: break; case UserPreferenceCategory.Screensaver: break; case UserPreferenceCategory.Window: break; case UserPreferenceCategory.Locale: break; case UserPreferenceCategory.VisualStyle: break; } }
همینطور که میبینید دسته بندی شامل موارد مختلفی میشه که به بخش های مختلف تنظیمات مربوطه تنظیمات مربوط به تم درون General صدا زده میشه پس کدهای ما قراره وارد این قسمت بشه.
متاسفانه این ایونت اطلاعات کاملتری رو به ما نمیده و فقط اطلاع میده که اینجا یه تغییری رخ داده (همینطور که قبلا گفتم هرچیزی یه هزینه ای داره?) پس ما خودمون میایم مشخصات تم فعلی رو دریافت میکنیم اما از کجا؟ معلومه رجیستری! همه چیز توی رجیستری ثبت میشه و براحتی قابل دسترسی هست پس یه متد مینویسم که کلید رجیستری مربوطه رو بخونه و اونو بصورت یه مدل (Light یا Dark) برگردونه:
private const string RegistryKeyPathTheme = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" private const string RegSysMode = "SystemUsesLightTheme" public static UITheme GetWindowsTheme() { return GetThemeFromRegistry(RegSysMode); } private static UITheme GetThemeFromRegistry(string registryKey) { using var key = Registry.CurrentUser.OpenSubKey(RegistryKeyPathTheme); var themeValue = key?.GetValue(registryKey) as int?; return themeValue != 0 ? UITheme.Light : UITheme.Dark; } public enum UITheme { Light, Dark }
حالا ما نیاز به یه ایونت داریم که کاربر بتونه اونو توی برنامه خودش رجیستر کنه و نیازی به پیاده سازی این کدها نداشته باشه پس یه EventHandler به اسم WindowsThemeChanged ایجاد میکنیم:
public event EventHandler<FunctionEventArgs<UIWindowTheme>> WindowsThemeChanged; protected virtual void OnWindowsThemeChanged(UIWindowTheme theme) { EventHandler<FunctionEventArgs<UIWindowTheme>> handler = WindowsThemeChanged; handler?.Invoke(this, new FunctionEventArgs<UIWindowTheme>(theme)); }
من میخام که کاربر، مقدار تم فعلی و رنگ Accent فعلی رو بتونه از طریق این ایونت دریافت بکنه پس من باید خودم یه EventArgs ایجاد بکنم و پراپرتی های دلخواه من رو داشته باشه برای همین کلاس FunctionEventArgs رو ایجاد میکنم:
public class FunctionEventArgs<T> : RoutedEventArgs { public FunctionEventArgs(T theme) { Theme = theme; } public FunctionEventArgs(RoutedEvent routedEvent, object source) : base(routedEvent, source){} public T Theme { get; set; } }
این کلاس از RoutedEventArgs ارث بری کرده و بصورت جنریک پیاده سازی شده، به این معنی که ما میتونیم هر نوع دلخواهی که خواستیم به عنوان arg استفاده بکنیم? اگر دقت بکنید من یک مدل دلخواه رو به عنوان arg مشخص کردم:
FunctionEventArgs<UIWindowTheme>
من میتونستم از همون UITheme هم استفاده بکنم ولی نمیتونستم مقدار Accent رو هم به کاربر برگردونم واسه همین یه مدل به اسم UIWindowTheme ساختم:
public class UIWindowTheme { public Brush AccentBrush { get; set; } public UITheme CurrentTheme { get; set; } }
کار تمومه حالا باید بیایم داخل متد SystemEvents_UserPreferenceChanged و بگیم که وقتی تنظیمات ویندوز عوض شد چه اتفاقی بیوفته (دقت کنید که کد باید داخل بخش General نوشته بشه):
case UserPreferenceCategory.General: var changedTheme = new UIWindowTheme() { AccentBrush = SystemParameters.WindowGlassBrush, CurrentTheme = GetWindowsTheme() }; OnWindowsThemeChanged(changedTheme); break;
یه مدل ایجاد میکنیم و مقدار AccentBrush رو برابر با WindowGlassBrush قرار میدیم این پراپرتی هم مثل ایونتی که اول معرفی کردم مربوط میشه به سیستم عامل و رنگ فعلی Accent رو برمیگردونه، برای مقدار CurrentTheme هم متدی که بالاتر برای دریافت تم فعلی از رجیستری نوشتیم رو صدا میزنیم و در پایان این مدل رو به ایونتمون پاس میدیم.
حالا اگر بریم سراغ یه برنامه دمو به این صورت میتونیم پیاده سازی کنیم:
ThemeHelper tm = new ThemeHelper(); tm.WindowsThemeChanged +=OnWindowsThemeChanged;
و
private void OnWindowsThemeChanged(object? sender, FunctionEventArgs<RegistryThemeHelper.UIWindowTheme> e) { rec.Fill = e.Theme.AccentBrush; if (e.Theme.CurrentTheme == RegistryThemeHelper.UITheme.Light) { Background = Brushes.White; } else { Background = Brushes.Black; } }
اینم از نتیجه: