Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/System.Windows.Forms/System/Windows/Forms/Form.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4961,10 +4961,23 @@ internal override unsafe void RecreateHandleCore()
GC.KeepAlive(this);
}

private void SetFormTitleProperties()
private unsafe 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)
{
BOOL isDark = Application.IsDarkModeEnabled;
PInvoke.DwmSetWindowAttribute(
HWND,
DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE,
&isDark,
(uint)sizeof(BOOL)).AssertSuccess();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider extracting this new DWM call into a small private helper (e.g. SetFormImmersiveDarkModeInternal(bool isDarkMode)), mirroring the existing SetFormAttributeColorInternal:

private unsafe void SetFormImmersiveDarkModeInternal(bool isDarkMode)
{
    BOOL isDark = isDarkMode;
    PInvoke.DwmSetWindowAttribute(
        HWND,
        DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE,
        &isDark,
        (uint)sizeof(BOOL));
}

and call it here:

if (Application.ColorModeSet && DarkModeRequestState is true)
{
    SetFormImmersiveDarkModeInternal(Application.IsDarkModeEnabled);
}

Benefits:

  1. Keeps SetFormTitleProperties() non-unsafe. The only reason this method's signature was changed to unsafe is the &isDark pointer. Moving that into a dedicated helper (as SetFormAttributeColorInternal already does with &colorRef) confines unsafe to the small helper and narrows the unsafe scope of the rest of the method.
  2. Consistency/readability. It parallels the neighboring SetFormAttributeColorInternal calls, with the guard condition kept in the caller (same pattern as the Properties.TryGetValue(...) checks below).
  3. Lets us drop .AssertSuccess(). The adjacent SetFormAttributeColorInternal deliberately ignores the HRESULT. AssertSuccess() is Debug.Assert(Succeeded, ...), and DWMWA_USE_IMMERSIVE_DARK_MODE (attribute 20) is unsupported on Windows 10 builds < 19041, where the call can return a failure HRESULT and trip a debug assertion. Matching the surrounding "ignore HRESULT" pattern avoids that.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion—agreed. I’ll extract the DWM call into a private SetFormImmersiveDarkModeInternal helper (mirroring SetFormAttributeColorInternal), keep SetFormTitleProperties() non-unsafe, and drop .AssertSuccess() so we ignore the HRESULT consistently.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. We should take this after those changes.

}

if (Properties.TryGetValue(s_propFormBorderColor, out Color? formBorderColor))
{
SetFormAttributeColorInternal(DWMWINDOWATTRIBUTE.DWMWA_BORDER_COLOR, formBorderColor.Value);
Expand Down
119 changes: 119 additions & 0 deletions src/test/unit/System.Windows.Forms/System/Windows/Forms/FormTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.ComponentModel;
using System.Drawing;
using Moq;
using Windows.Win32.Graphics.Dwm;

namespace System.Windows.Forms.Tests;

Expand Down Expand Up @@ -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<object[]> Visible_Set_TestData()
{
foreach (DialogResult dialogResult in Enum.GetValues(typeof(DialogResult)))
Expand Down
Loading