diff --git a/src/System.Windows.Forms/System/Windows/Forms/Form.cs b/src/System.Windows.Forms/System/Windows/Forms/Form.cs index c3900d09c6b..21e2a6a6360 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Form.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Form.cs @@ -4965,6 +4965,14 @@ private void SetFormTitleProperties() { if (TopLevel && IsHandleCreated) { + // Re-apply the dark-mode title bar flag. DWM window attributes are not preserved + // across handle recreation, so DWMWA_USE_IMMERSIVE_DARK_MODE must be restored + // here alongside the other DWM attributes. + if (Application.ColorModeSet && DarkModeRequestState is true) + { + SetFormImmersiveDarkModeInternal(Application.IsDarkModeEnabled); + } + if (Properties.TryGetValue(s_propFormBorderColor, out Color? formBorderColor)) { SetFormAttributeColorInternal(DWMWINDOWATTRIBUTE.DWMWA_BORDER_COLOR, formBorderColor.Value); @@ -4987,6 +4995,16 @@ private void SetFormTitleProperties() } } + private unsafe void SetFormImmersiveDarkModeInternal(bool isDarkMode) + { + BOOL isDark = isDarkMode; + PInvoke.DwmSetWindowAttribute( + HWND, + DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, + &isDark, + (uint)sizeof(BOOL)); + } + /// /// Removes a form from the list of owned forms. Also sets the owner of the /// removed form to null. diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/FormTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/FormTests.cs index 26574491052..624108d3bc7 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/FormTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/FormTests.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Drawing; using Moq; +using Windows.Win32.Graphics.Dwm; namespace System.Windows.Forms.Tests; @@ -1886,6 +1887,124 @@ public void Form_ShowInTaskbar_SetFalse_GetReturnsExpected() Assert.Equal(expectedDialogResult, form.DialogResult); } + [WinFormsFact] + public void Form_RecreateHandle_ReappliesImmersiveDarkModeTitleBar() + { + // Regression for https://github.com/dotnet/winforms/issues/12582 + // Avoid Application.SetColorMode — can hang in NotifySystemEventsOfColorChange + // after a form has created the SystemEvents broadcast window. + + if (SystemInformation.HighContrast) + { + return; + } + + var applicationAccessor = typeof(Application).TestAccessor.Dynamic; + SystemColorMode? previousColorMode = applicationAccessor.s_colorMode; + + try + { + applicationAccessor.s_colorMode = SystemColorMode.Dark; + Assert.True(Application.ColorModeSet); + Assert.True(Application.IsDarkModeEnabled); + + using Form form = new(); + form.TestAccessor.Dynamic.DarkModeRequestState = true; + + form.Show(); + Assert.True(form.IsHandleCreated); + + Assert.True( + TryGetUseImmersiveDarkMode(form.HWND, out bool darkBefore) && darkBefore, + "Expected immersive dark mode when the form is first shown."); + + IntPtr handleBefore = form.Handle; + + form.RightToLeft = RightToLeft.Yes; + + Assert.True(form.IsHandleCreated); + Assert.NotEqual(handleBefore, form.Handle); + + Assert.True( + TryGetUseImmersiveDarkMode(form.HWND, out bool darkAfter) && darkAfter, + "Expected immersive dark mode restored after handle recreation."); + + form.Close(); + } + finally + { + applicationAccessor.s_colorMode = previousColorMode; + } + } + + [WinFormsFact] + public void Form_ShowInTaskbar_Change_ReappliesImmersiveDarkModeTitleBar() + { + // Regression for https://github.com/dotnet/winforms/issues/12992 + // Same root cause as #12582: handle recreation must re-apply + // DWMWA_USE_IMMERSIVE_DARK_MODE via Form.SetFormTitleProperties. + // Avoid Application.SetColorMode — can hang in NotifySystemEventsOfColorChange. + + if (SystemInformation.HighContrast) + { + return; + } + + var applicationAccessor = typeof(Application).TestAccessor.Dynamic; + SystemColorMode? previousColorMode = applicationAccessor.s_colorMode; + + try + { + applicationAccessor.s_colorMode = SystemColorMode.Dark; + Assert.True(Application.ColorModeSet); + Assert.True(Application.IsDarkModeEnabled); + + using Form form = new() + { + ShowInTaskbar = true, + }; + form.TestAccessor.Dynamic.DarkModeRequestState = true; + + form.Show(); + Assert.True(form.IsHandleCreated); + + Assert.True( + TryGetUseImmersiveDarkMode(form.HWND, out bool darkBefore) && darkBefore, + "Expected immersive dark mode when the form is first shown."); + + IntPtr handleBefore = form.Handle; + + // Repro path from #12992 (toggle force recreate, same as product UI). + form.ShowInTaskbar = false; + + Assert.True(form.IsHandleCreated); + Assert.NotEqual(handleBefore, form.Handle); + + Assert.True( + TryGetUseImmersiveDarkMode(form.HWND, out bool darkAfter) && darkAfter, + "Expected immersive dark mode restored after ShowInTaskbar forces handle recreation."); + + form.Close(); + } + finally + { + applicationAccessor.s_colorMode = previousColorMode; + } + } + + private static unsafe bool TryGetUseImmersiveDarkMode(HWND hwnd, out bool isDark) + { + BOOL value = false; + HRESULT hr = PInvoke.DwmGetWindowAttribute( + hwnd, + DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, + &value, + (uint)sizeof(BOOL)); + + isDark = value; + return hr.Succeeded; + } + public static IEnumerable Visible_Set_TestData() { foreach (DialogResult dialogResult in Enum.GetValues(typeof(DialogResult)))