Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dark mode/Visual Styles (VB Only) fix #11907

Merged
merged 6 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
49 changes: 25 additions & 24 deletions src/System.Windows.Forms/src/System/Windows/Forms/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public sealed partial class Application

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;

/// <summary>
/// Events the user can hook into
Expand Down Expand Up @@ -252,11 +252,7 @@ internal static bool CustomThreadExceptionHandlerAttached
/// </summary>
[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_systemColorMode ?? SystemColorMode.Classic;

/// <summary>
/// Sets the default dark mode for the application.
Expand All @@ -281,12 +277,19 @@ public static void SetColorMode(SystemColorMode systemColorMode)
return;
}

if (GetSystemColorModeInternal() > -1)
if ((systemColorMode == SystemColorMode.Dark && IsSystemDarkModeAvailable)
|| systemColorMode == SystemColorMode.Classic)
{
s_systemColorMode = systemColorMode;
return;
}

if (GetSystemColorModeInternal() > SystemDarkModeDisabled)
{
s_systemColorMode = SystemColorMode.Dark;
return;
}

s_systemColorMode = SystemColorMode.Classic;
}
finally
Expand Down Expand Up @@ -352,35 +355,33 @@ static void NotifySystemEventsOfColorChange()
? SystemColorMode.Dark
: SystemColorMode.Classic;

// Returns 0 if dark mode is available, otherwise -1 (DarkModeNotAvailable)
// Returns 0 if dark mode is enabled in the system, otherwise -1 (SystemDarkModeDisabled)
private static int GetSystemColorModeInternal()
{
if (SystemInformation.HighContrast)
if (!IsSystemDarkModeAvailable)
{
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
{
systemColorMode = (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;
}

private static bool IsSystemDarkModeAvailable =>
!SystemInformation.HighContrast && OsVersion.IsWindows11_OrGreater();

/// <summary>
/// Gets a value indicating whether the application is running in a dark system color context.
/// Note: In a high contrast mode, this will always return <see langword="false"/>.
Expand Down
145 changes: 133 additions & 12 deletions src/System.Windows.Forms/src/System/Windows/Forms/Form.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,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.
Expand Down Expand Up @@ -2314,8 +2318,21 @@ protected override void SetVisibleCore(bool value)
}

/// <summary>
/// Sets or gets the rounding style of the corners using the <see cref="FormCornerPreference"/> enum.
/// Sets or gets the rounding style of the Form's corners using the <see cref="FormCornerPreference"/> enum.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// The property only reflects the value that was previously set using this property. The
/// <see cref="FormCornerPreferenceChanged"/> event is raised accordingly when the value is
/// changed, which allows the property to be participating in binding scenarios.
/// </para>
/// </remarks>
[DefaultValue(FormCornerPreference.Default)]
[SRCategory(nameof(SR.CatWindowStyle))]
[SRDescription(nameof(SR.FormCornerPreferenceDescr))]
Expand All @@ -2341,7 +2358,14 @@ public FormCornerPreference FormCornerPreference
_ => throw new ArgumentOutOfRangeException(nameof(value))
};

Properties.AddOrRemoveValue(s_propFormCornerPreference, value);
if (value == FormCornerPreference.Default)
{
Properties.RemoveValue(s_propFormCornerPreference);
}
else
{
Properties.AddValue(s_propFormCornerPreference, value);
}

if (IsHandleCreated)
{
Expand All @@ -2356,6 +2380,9 @@ public FormCornerPreference FormCornerPreference
/// Raises the <see cref="FormCornerPreferenceChanged"/> event when the
/// <see cref="FormCornerPreference"/> property changes.
/// </summary>
/// <param name="e">
/// An <see cref="EventArgs"/> that contains the event data, in this case empty.
/// </param>
[Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)]
protected virtual void OnFormCornerPreferenceChanged(EventArgs e)
{
Expand Down Expand Up @@ -2388,21 +2415,41 @@ private unsafe void SetFormCornerPreferenceInternal(FormCornerPreference cornerP
/// <summary>
/// Sets or gets the Form's border color.
/// </summary>
/// <returns>
/// The <see cref="Color"/> which has be previously set using this property or <see cref="Color.Empty"/>.
/// Note that the underlying Win32 API does not provide a reliable mechanism to retrieve the current
/// border color.
/// </returns>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// The property only reflects the value that was previously set using this property. The
/// <see cref="FormBorderColorChanged"/> event is raised accordingly when the value is
/// changed, which allows the property to be participating in binding scenarios.
/// </para>
/// </remarks>
[SRCategory(nameof(SR.CatWindowStyle))]
[SRDescription(nameof(SR.FormBorderColorDescr))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)]
Copy link

@memoarfaa memoarfaa Aug 18, 2024

Choose a reason for hiding this comment

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

@KlausLoeffelmann
I see this function need to be in some Controls not only in Form So that we can benefit from it to the maximum extent

Choose a reason for hiding this comment

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

this function can be applied to all popup

this function can be applied to all popup Windows

like Menus ,Tooltips, ...........

Non round example

2024-08-18_04-42-29.mp4

Round example

2024-08-18_04-51-13.mp4

Copy link
Member Author

Choose a reason for hiding this comment

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

Wait, what do you mean "this function can be applied..."
When I see the video clips, I see I few things I have questions about. Like the dark Tab Control. How is that implemented/tuned?

The rounded Buttons look cool (and also the tool tips and other elements I see).
Is this done by custom rendering or did you apply the same Win32 API to it?

Keep in mind:
VisualStylesMode was a concept I came up with primarily driven by accessibility, actually somewhat late in the development cycle (around end of April/begin of May I think I only had the first prototype which I did as a proof of concept first in my spare time) to complement the dark mode feature in terms of contrast issues we saw, and in general to address an upcoming regulatory issue where we need bigger "real estate" for clicking "landing spots" for controls. So, the teeny-tiny Up-Down-Buttons from NumericUpDown or DomainUpDown are very extreme examples for that. They are just an accessibility nightmare IMO. The non-styleble MonthCalendar with its tiny date numbers is another example. And TextBox (TextBoxBase) itself is yet another one, because we cannot adjust its Padding: In 96 dpi without a border, it got only a height of 16 pixels, if I remember correctly. And that's just not good enough in the context of HighDPI-Monitors and mindful accessibility. So - "styling" improvements should have those things in mind. The animated CheckBox looks arguably cooler than the old one as a side effect, alright: But: It has that bigger landing spot and is better recognizable. We need to have a valid argument in that regard to think about improvements in style or real-estate.

Everything that goes beyond that becomes Theming, and that's where we (WinForms Team) all agreed, is a line we cannot cross. Our third parties have those things already in place, and they do an extremely good job with that.

Copy link

@memoarfaa memoarfaa Aug 22, 2024

Choose a reason for hiding this comment

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

Wait, what do you mean "this function can be applied..." When I see the video clips, I see I few things I have questions about. Like the dark Tab Control. How is that implemented/tuned?

this is implemented by paint on Wm_ Paint but there is multiple solution for the Tab Control see #11953.

The rounded Buttons look cool (and also the tool tips and other elements I see). Is this done by custom rendering or did you apply the same Win32 API to it?

The tool tips and pop up menus use DWM_WINDOW_CORNER_PREFERENCE and any Window has WS_POPUP can use it

#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.
            if (SystemInformation.HighContrast)
            {
                PInvoke.SetWindowTheme(HWND, string.Empty, string.Empty);
            }
            else if (Application.IsDarkModeEnabled && !_isBalloon)
            {
                DWM_WINDOW_CORNER_PREFERENCE round = DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND;
                PInvoke.DwmSetWindowAttribute(HWND, DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE, &round, sizeof(DWM_WINDOW_CORNER_PREFERENCE));
                PInvoke.SendMessage(HWND, PInvoke.TTM_SETWINDOWTHEME, default, "DarkMode_Explorer");
            }

#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.
        }

the remains elements is use Uxtheme Win32 API and its Documented.

Everything that goes beyond that becomes Theming, and that's where we (WinForms Team) all agreed, is a line we cannot cross. Our third parties have those things already in place, and they do an extremely good job with that.

may me not one line but multiple lines

public Color FormBorderColor
{
get => GetFormAttributeColorInternal(DWMWINDOWATTRIBUTE.DWMWA_BORDER_COLOR);
get => Properties.GetValueOrDefault(s_propFormBorderColor, Color.Empty);
set
{
if (value == FormBorderColor)
{
return;
}

Properties.AddValue(s_propFormBorderColor, value);

if (IsHandleCreated)
{
SetFormAttributeColorInternal(DWMWINDOWATTRIBUTE.DWMWA_BORDER_COLOR, value);
Expand All @@ -2415,6 +2462,9 @@ public Color FormBorderColor
/// <summary>
/// Raises the <see cref="FormBorderColorChanged"/> event when the <see cref="FormBorderColor"/> property changes.
/// </summary>
/// <param name="e">
/// An <see cref="EventArgs"/> that contains the event data, in this case empty.
/// </param>
[Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)]
protected virtual void OnFormBorderColorChanged(EventArgs e)
{
Expand All @@ -2425,23 +2475,43 @@ protected virtual void OnFormBorderColorChanged(EventArgs e)
}

/// <summary>
/// Sets or gets the Form's title bar back color.
/// Sets or gets the Form's title bar back color (caption back color).
/// </summary>
/// <returns>
/// The <see cref="Color"/>, which has be previously set using this property or <see cref="Color.Empty"/>.
/// Note that the underlying Win32 API does not provide a reliable mechanism to retrieve the current title
/// bar color.
/// </returns>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// The property only reflects the value that was previously set using this property. The
/// <see cref="FormCaptionBackColorChanged"/> event is raised accordingly when the value is
/// changed, which allows the property to be participating in binding scenarios.
/// </para>
/// </remarks>
[SRCategory(nameof(SR.CatWindowStyle))]
[SRDescription(nameof(SR.FormCaptionBackColorDescr))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)]
public Color FormCaptionBackColor
{
get => GetFormAttributeColorInternal(DWMWINDOWATTRIBUTE.DWMWA_CAPTION_COLOR);
get => Properties.GetValueOrDefault(s_propFormCaptionBackColor, Color.Empty);
set
{
if (value == FormCaptionBackColor)
{
return;
}

Properties.AddValue(s_propFormCaptionBackColor, value);

if (IsHandleCreated)
{
SetFormAttributeColorInternal(DWMWINDOWATTRIBUTE.DWMWA_CAPTION_COLOR, value);
Expand All @@ -2452,8 +2522,12 @@ public Color FormCaptionBackColor
}

/// <summary>
/// Raises the <see cref="FormCaptionBackColor"/> event when the <see cref="FormCaptionBackColor"/> property changes.
/// Raises the <see cref="FormCaptionBackColorChanged"/> event when the <see cref="FormCaptionBackColor"/>
/// property changes.
/// </summary>
/// <param name="e">
/// An <see cref="EventArgs"/> that contains the event data, in this case empty.
/// </param>
[Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)]
protected virtual void OnFormCaptionBackColorChanged(EventArgs e)
{
Expand All @@ -2464,23 +2538,43 @@ protected virtual void OnFormCaptionBackColorChanged(EventArgs e)
}

/// <summary>
/// Sets or gets the Form's title bar back color.
/// Sets or gets the Form's title bar text color (windows caption text color).
/// </summary>
/// <returns>
/// The <see cref="Color"/>, which has be previously set using this property or <see cref="Color.Empty"/>.
/// Note that the underlying Win32 API does not provide a reliable mechanism to retrieve the current title
/// bar text color.
/// </returns>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// The property only reflects the value that was previously set using this property. The
/// <see cref="FormCaptionTextColorChanged"/> event is raised accordingly when the value is
/// changed, which allows the property to be participating in binding scenarios.
/// </para>
/// </remarks>
[SRCategory(nameof(SR.CatWindowStyle))]
[SRDescription(nameof(SR.FormCaptionTextColorDescr))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)]
public Color FormCaptionTextColor
{
get => GetFormAttributeColorInternal(DWMWINDOWATTRIBUTE.DWMWA_TEXT_COLOR);
get => Properties.GetValueOrDefault(s_propFormCaptionTextColor, Color.Empty);
set
{
if (value == FormCaptionTextColor)
{
return;
}

Properties.AddValue(s_propFormCaptionTextColor, value);

if (IsHandleCreated)
{
SetFormAttributeColorInternal(DWMWINDOWATTRIBUTE.DWMWA_TEXT_COLOR, value);
Expand All @@ -2491,12 +2585,16 @@ public Color FormCaptionTextColor
}

/// <summary>
/// Raises the <see cref="FormCaptionTextColor"/> event when the <see cref="FormCaptionTextColor"/> property changes.
/// Raises the <see cref="FormCaptionTextColorChanged"/> event when the
/// <see cref="FormCaptionTextColor"/> property changes.
/// </summary>
/// <param name="e">
/// An <see cref="EventArgs"/> that contains the event data, in this case empty.
/// </param>
[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);
}
Expand Down Expand Up @@ -4211,6 +4309,29 @@ protected override void OnHandleCreated(EventArgs e)
{
_formStateEx[s_formStateExUseMdiChildProc] = (IsMdiChild && Visible) ? 1 : 0;
base.OnHandleCreated(e);

if (Properties.TryGetValue(s_propFormBorderColor, out Color? formBorderColor))
{
SetFormAttributeColorInternal(DWMWINDOWATTRIBUTE.DWMWA_BORDER_COLOR, formBorderColor.Value);
}

if (Properties.TryGetValue(s_propFormCaptionBackColor, out Color? formCaptionBackColor))
{
SetFormAttributeColorInternal(DWMWINDOWATTRIBUTE.DWMWA_CAPTION_COLOR, formCaptionBackColor.Value);
}

if (Properties.TryGetValue(s_propFormCaptionTextColor, out Color? formCaptionTextColor))
{
SetFormAttributeColorInternal(DWMWINDOWATTRIBUTE.DWMWA_TEXT_COLOR, formCaptionTextColor.Value);
}

#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.
if (Properties.TryGetValue(s_propFormCornerPreference, out FormCornerPreference? cornerPreference))
{
SetFormCornerPreferenceInternal(cornerPreference.Value);
}
#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.

UpdateLayered();
}

Expand Down Expand Up @@ -5814,7 +5935,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.
/// </para>
/// <para>
/// If the form is already displayed asynchronously by <see cref="Form.ShowAsync"/>, an <see cref="InvalidOperationException"/> will be thrown.
/// If the form is already displayed asynchronously by <see cref="ShowAsync"/>, an <see cref="InvalidOperationException"/> will be thrown.
/// </para>
/// <para>
/// An <see cref="InvalidOperationException"/> will also occur if no <see cref="WindowsFormsSynchronizationContext"/> could be retrieved or installed.
Expand Down Expand Up @@ -5849,7 +5970,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.
/// </para>
/// <para>
/// If the form is already displayed asynchronously by <see cref="Form.ShowAsync"/>, an <see cref="InvalidOperationException"/> will be thrown.
/// If the form is already displayed asynchronously by <see cref="ShowAsync"/>, an <see cref="InvalidOperationException"/> will be thrown.
/// </para>
/// <para>
/// An <see cref="InvalidOperationException"/> will also occur if no <see cref="WindowsFormsSynchronizationContext"/> could be retrieved or installed.
Expand Down
Loading