From f9b3ed3ad8b0626637fbbd95ed25fbc31c439f4e Mon Sep 17 00:00:00 2001 From: Klaus Loeffelmann Date: Fri, 16 Aug 2024 18:44:17 -0700 Subject: [PATCH] Fix dark mode issues. Add plausibility unit tests. Apply backwards-compatibility (10->9) fix. Refactor System-handling for SetColor. Fix XML comments and property value handling in Formxxx properties. Fix bug where in Visual Basic VisualStyles no longer get applied. Fix raising wrong event on changing FormCaptionTextColor. --- .../WindowsFormsApplicationBase.vb | 4 + .../src/System/Windows/Forms/Application.cs | 88 +++++++------ .../src/System/Windows/Forms/Form.cs | 121 ++++++++++++++++-- .../System/Windows/Forms/ApplicationTests.cs | 31 +++++ 4 files changed, 197 insertions(+), 47 deletions(-) diff --git a/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/ApplicationServices/WindowsFormsApplicationBase.vb b/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/ApplicationServices/WindowsFormsApplicationBase.vb index 4882984446e..1751e06924d 100644 --- a/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/ApplicationServices/WindowsFormsApplicationBase.vb +++ b/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/ApplicationServices/WindowsFormsApplicationBase.vb @@ -550,6 +550,10 @@ Namespace Microsoft.VisualBasic.ApplicationServices Debug.Assert(dpiSetResult, "We could net set the HighDpiMode.") ' Now, let's set VisualStyles and ColorMode: + If (_enableVisualStyles) Then + Application.EnableVisualStyles() + End If + Application.SetColorMode(_colorMode) #Enable Warning WFO5001 ' Type is for evaluation purposes only and is subject to change or removal in future updates. diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/Application.cs b/src/System.Windows.Forms/src/System/Windows/Forms/Application.cs index a249cc47210..8f249aad8d3 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/Application.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/Application.cs @@ -45,12 +45,12 @@ public sealed partial class Application private static bool s_useWaitCursor; #pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - private static SystemColorMode? s_systemColorMode; + private static SystemColorMode? s_colorMode; #pragma warning restore WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. private const string DarkModeKeyPath = "HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; private const string DarkModeKey = "AppsUseLightTheme"; - private const int DarkModeNotAvailable = -1; + private const int SystemDarkModeDisabled = 1; /// /// Events the user can hook into @@ -247,27 +247,51 @@ internal static bool CustomThreadExceptionHandlerAttached => ThreadContext.FromCurrent().CustomThreadExceptionHandlerAttached; /// - /// Gets the default dark mode for the application. This is the SystemColorMode which either has been set - /// by or its default value . + /// Gets the default color mode (dark mode) for the application. /// + /// + /// + /// This is the which either has been set by + /// or its default value . If it has been set to , + /// then the actual color mode is determined by the system settings (which can be retrieved by the + /// static (shared in VB) property. + /// + /// [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public static SystemColorMode ColorMode => - !s_systemColorMode.HasValue - ? SystemColorMode.Classic - : s_systemColorMode.Value == SystemColorMode.System - ? SystemColorMode - : s_systemColorMode.Value; + s_colorMode ?? SystemColorMode.Classic; /// - /// Sets the default dark mode for the application. + /// Sets the default color mode (dark mode) for the application. /// - /// The default dark mode to set. + /// The application's default color mode (dark mode) to set. + /// + /// + /// You should use this method to set the default color mode (dark mode) for the application. Set it, + /// before creating any UI elements, to ensure that the correct color mode is used. You can set it to + /// dark mode (), light mode () + /// or to the system setting (). + /// + /// + /// If you set it to , the actual color mode is determined by the + /// Windows system settings. If the system setting is changed, the application will not automatically + /// adapt to the new setting. + /// + /// + /// Note that the dark color mode is only available from Windows 11 on or later versions. If the system + /// is set to a high contrast mode, the dark mode is not available. + /// + /// + /// Note for Visual Basic: If you are using the Visual Basic Application Framework, you should set the + /// color mode by handling the Application Events (see "WindowsFormsApplicationBase.ApplyApplicationDefaults"). + /// + /// [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public static void SetColorMode(SystemColorMode systemColorMode) { try { - // Can't use the Generator here, since it cannot deal with experimentals. + // Can't use the Generator here, since it cannot deal with [Experimental]. _ = systemColorMode switch { SystemColorMode.Classic => systemColorMode, @@ -276,18 +300,12 @@ public static void SetColorMode(SystemColorMode systemColorMode) _ => throw new ArgumentOutOfRangeException(nameof(systemColorMode)) }; - if (systemColorMode == s_systemColorMode) + if (systemColorMode == s_colorMode) { return; } - if (GetSystemColorModeInternal() > -1) - { - s_systemColorMode = systemColorMode; - return; - } - - s_systemColorMode = SystemColorMode.Classic; + s_colorMode = systemColorMode; } finally { @@ -315,6 +333,7 @@ static void NotifySystemEventsOfColorChange() bool complete = false; bool success = PInvoke.SendMessageCallback(hwnd, PInvoke.WM_SYSCOLORCHANGE + MessageId.WM_REFLECT, () => complete = true); Debug.Assert(success); + if (!success) { return; @@ -357,25 +376,21 @@ private static int GetSystemColorModeInternal() { if (SystemInformation.HighContrast) { - return DarkModeNotAvailable; + return SystemDarkModeDisabled; } - int systemColorMode = DarkModeNotAvailable; + int systemColorMode = SystemDarkModeDisabled; - // Dark mode is supported when we are >= W11/22000 - // Technically, we could go earlier, but then the APIs we're using weren't officially public. - if (OsVersion.IsWindows11_OrGreater()) + try + { + // 0 for dark mode and |1| for light mode. + systemColorMode = Math.Abs((Registry.GetValue( + keyName: DarkModeKeyPath, + valueName: DarkModeKey, + defaultValue: SystemDarkModeDisabled) as int?) ?? systemColorMode); + } + catch (Exception ex) when (!ex.IsCriticalException()) { - try - { - systemColorMode = (Registry.GetValue( - keyName: DarkModeKeyPath, - valueName: DarkModeKey, - defaultValue: DarkModeNotAvailable) as int?) ?? systemColorMode; - } - catch (Exception ex) when (!ex.IsCriticalException()) - { - } } return systemColorMode; @@ -388,7 +403,8 @@ private static int GetSystemColorModeInternal() [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public static bool IsDarkModeEnabled => !SystemInformation.HighContrast - && (ColorMode == SystemColorMode.Dark); + && (ColorMode == SystemColorMode.Dark + || (ColorMode == SystemColorMode.System && SystemColorMode == SystemColorMode.Dark)); /// /// Gets the path for the executable file that started the application. diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/Form.cs b/src/System.Windows.Forms/src/System/Windows/Forms/Form.cs index 281067c3515..c8b899ffb5a 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/Form.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/Form.cs @@ -137,6 +137,10 @@ public partial class Form : ContainerControl private static readonly int s_propOpacity = PropertyStore.CreateKey(); private static readonly int s_propTransparencyKey = PropertyStore.CreateKey(); private static readonly int s_propFormCornerPreference = PropertyStore.CreateKey(); + private static readonly int s_propFormBorderColor = PropertyStore.CreateKey(); + + private static readonly int s_propFormCaptionTextColor = PropertyStore.CreateKey(); + private static readonly int s_propFormCaptionBackColor = PropertyStore.CreateKey(); // Form per instance members // Note: Do not add anything to this list unless absolutely necessary. @@ -2344,8 +2348,21 @@ protected override void SetVisibleCore(bool value) } /// - /// Sets or gets the rounding style of the corners using the enum. + /// Sets or gets the rounding style of the Form's corners using the enum. /// + /// + /// + /// Note: Reading this property is only for tracking purposes. If the Form's corner preference is + /// changed through other external means (Win32 calls), reading this property will not reflect + /// those changes, as the Win32 API does not provide a mechanism to retrieve the current title + /// bar color. + /// + /// + /// The property only reflects the value that was previously set using this property. The + /// event is raised accordingly when the value is + /// changed, which allows the property to be participating in binding scenarios. + /// + /// [DefaultValue(FormCornerPreference.Default)] [SRCategory(nameof(SR.CatWindowStyle))] [SRDescription(nameof(SR.FormCornerPreferenceDescr))] @@ -2395,6 +2412,9 @@ public FormCornerPreference FormCornerPreference /// Raises the event when the /// property changes. /// + /// + /// An that contains the event data, in this case empty. + /// [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] protected virtual void OnFormCornerPreferenceChanged(EventArgs e) { @@ -2427,6 +2447,24 @@ private unsafe void SetFormCornerPreferenceInternal(FormCornerPreference cornerP /// /// Sets or gets the Form's border color. /// + /// + /// The which has be previously set using this property or . + /// Note that the underlying Win32 API does not provide a reliable mechanism to retrieve the current + /// border color. + /// + /// + /// + /// Note: Reading this property is only for tracking purposes. If the Form's border color is + /// changed through other external means (Win32 calls), reading this property will not reflect + /// those changes, as the Win32 API does not provide a mechanism to retrieve the current title + /// bar color. + /// + /// + /// The property only reflects the value that was previously set using this property. The + /// event is raised accordingly when the value is + /// changed, which allows the property to be participating in binding scenarios. + /// + /// [SRCategory(nameof(SR.CatWindowStyle))] [SRDescription(nameof(SR.FormBorderColorDescr))] [Browsable(false)] @@ -2434,7 +2472,10 @@ private unsafe void SetFormCornerPreferenceInternal(FormCornerPreference cornerP [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public Color FormBorderColor { - get => GetFormAttributeColorInternal(DWMWINDOWATTRIBUTE.DWMWA_BORDER_COLOR); + get => Properties.ContainsObject(s_propFormBorderColor) + ? Properties.GetColor(s_propFormBorderColor) + : Color.Empty; + set { if (value == FormBorderColor) @@ -2442,6 +2483,8 @@ public Color FormBorderColor return; } + Properties.SetColor(s_propFormBorderColor, value); + if (IsHandleCreated) { SetFormAttributeColorInternal(DWMWINDOWATTRIBUTE.DWMWA_BORDER_COLOR, value); @@ -2454,6 +2497,9 @@ public Color FormBorderColor /// /// Raises the event when the property changes. /// + /// + /// An that contains the event data, in this case empty. + /// [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] protected virtual void OnFormBorderColorChanged(EventArgs e) { @@ -2464,8 +2510,26 @@ protected virtual void OnFormBorderColorChanged(EventArgs e) } /// - /// Sets or gets the Form's title bar back color. + /// Sets or gets the Form's title bar back color (caption back color). /// + /// + /// The , which has be previously set using this property or . + /// Note that the underlying Win32 API does not provide a reliable mechanism to retrieve the current title + /// bar color. + /// + /// + /// + /// Note: Reading this property is only for tracking purposes. If the window's title bar color is + /// changed through other external means (Win32 calls), reading this property will not reflect + /// those changes, as the Win32 API does not provide a mechanism to retrieve the current title + /// bar color. + /// + /// + /// The property only reflects the value that was previously set using this property. The + /// event is raised accordingly when the value is + /// changed, which allows the property to be participating in binding scenarios. + /// + /// [SRCategory(nameof(SR.CatWindowStyle))] [SRDescription(nameof(SR.FormCaptionBackColorDescr))] [Browsable(false)] @@ -2473,7 +2537,10 @@ protected virtual void OnFormBorderColorChanged(EventArgs e) [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public Color FormCaptionBackColor { - get => GetFormAttributeColorInternal(DWMWINDOWATTRIBUTE.DWMWA_CAPTION_COLOR); + get => Properties.ContainsObject(s_propFormCaptionBackColor) + ? Properties.GetColor(s_propFormCaptionBackColor) + : Color.Empty; + set { if (value == FormCaptionBackColor) @@ -2481,6 +2548,8 @@ public Color FormCaptionBackColor return; } + Properties.SetColor(s_propFormCaptionBackColor, value); + if (IsHandleCreated) { SetFormAttributeColorInternal(DWMWINDOWATTRIBUTE.DWMWA_CAPTION_COLOR, value); @@ -2491,8 +2560,12 @@ public Color FormCaptionBackColor } /// - /// Raises the event when the property changes. + /// Raises the event when the + /// property changes. /// + /// + /// An that contains the event data, in this case empty. + /// [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] protected virtual void OnFormCaptionBackColorChanged(EventArgs e) { @@ -2503,8 +2576,26 @@ protected virtual void OnFormCaptionBackColorChanged(EventArgs e) } /// - /// Sets or gets the Form's title bar back color. + /// Sets or gets the Form's title bar text color (windows caption text color). /// + /// + /// The , which has be previously set using this property or . + /// Note that the underlying Win32 API does not provide a reliable mechanism to retrieve the current title + /// bar text color. + /// + /// + /// + /// Note: Reading this property is only for tracking purposes. If the Form's title bar's text color + /// (window caption text) is changed through other external means (Win32 calls), reading this property + /// will not reflect those changes, as the Win32 API does not provide a mechanism to retrieve the + /// current title bar color. + /// + /// + /// The property only reflects the value that was previously set using this property. The + /// event is raised accordingly when the value is + /// changed, which allows the property to be participating in binding scenarios. + /// + /// [SRCategory(nameof(SR.CatWindowStyle))] [SRDescription(nameof(SR.FormCaptionTextColorDescr))] [Browsable(false)] @@ -2512,7 +2603,9 @@ protected virtual void OnFormCaptionBackColorChanged(EventArgs e) [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public Color FormCaptionTextColor { - get => GetFormAttributeColorInternal(DWMWINDOWATTRIBUTE.DWMWA_TEXT_COLOR); + get => Properties.ContainsObject(s_propFormCaptionTextColor) + ? Properties.GetColor(s_propFormCaptionTextColor) + : Color.Empty; set { if (value == FormCaptionTextColor) @@ -2520,6 +2613,8 @@ public Color FormCaptionTextColor return; } + Properties.SetColor(s_propFormCaptionTextColor, value); + if (IsHandleCreated) { SetFormAttributeColorInternal(DWMWINDOWATTRIBUTE.DWMWA_TEXT_COLOR, value); @@ -2530,12 +2625,16 @@ public Color FormCaptionTextColor } /// - /// Raises the event when the property changes. + /// Raises the event when the + /// property changes. /// + /// + /// An that contains the event data, in this case empty. + /// [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] protected virtual void OnFormCaptionTextColorChanged(EventArgs e) { - if (Events[s_formCaptionBackColorChanged] is EventHandler eventHandler) + if (Events[s_formCaptionTextColorChanged] is EventHandler eventHandler) { eventHandler(this, e); } @@ -5854,7 +5953,7 @@ public DialogResult ShowDialog(IWin32Window? owner) /// This method immediately returns, even if the form is large and takes a long time to be set up. /// /// - /// If the form is already displayed asynchronously by , an will be thrown. + /// If the form is already displayed asynchronously by , an will be thrown. /// /// /// An will also occur if no could be retrieved or installed. @@ -5889,7 +5988,7 @@ public DialogResult ShowDialog(IWin32Window? owner) /// This method immediately returns, even if the form is large and takes a long time to be set up. /// /// - /// If the form is already displayed asynchronously by , an will be thrown. + /// If the form is already displayed asynchronously by , an will be thrown. /// /// /// An will also occur if no could be retrieved or installed. diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ApplicationTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ApplicationTests.cs index 5d39868002e..e343bcb57c6 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ApplicationTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ApplicationTests.cs @@ -146,6 +146,37 @@ public void Application_EnableVisualStyles_ManifestResourceExists() Assert.NotNull(stream); } +#pragma warning disable SYSLIB5002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + [Fact] + public void Application_SetColorMode_PlausibilityTests() + { + if (SystemInformation.HighContrast) + { + // We don't run this test in HighContrast mode. + return; + } + + SystemColorMode systemColorMode = Application.SystemColorMode; + + Application.SetColorMode(SystemColorMode.Classic); + Assert.False(Application.IsDarkModeEnabled); + Assert.Equal(SystemColorMode.Classic, Application.ColorMode); + Assert.False(SystemColors.UseAlternativeColorSet); + + Application.SetColorMode(SystemColorMode.Dark); + Assert.True(Application.IsDarkModeEnabled); + Assert.Equal(SystemColorMode.Dark, Application.ColorMode); + Assert.True(SystemColors.UseAlternativeColorSet); + + Application.SetColorMode(SystemColorMode.System); + Assert.False(Application.IsDarkModeEnabled ^ systemColorMode == SystemColorMode.Dark); + Assert.Equal(SystemColorMode.System, Application.ColorMode); + Assert.False(SystemColors.UseAlternativeColorSet ^ systemColorMode == SystemColorMode.Dark); + } +#pragma warning restore WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning restore SYSLIB5002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + [WinFormsFact] public void Application_DefaultFont_ReturnsNull_IfNoFontSet() {