diff --git a/docs/runtime-configuration.md b/docs/runtime-configuration.md index a578b6a7eac..85ca4472950 100644 --- a/docs/runtime-configuration.md +++ b/docs/runtime-configuration.md @@ -135,6 +135,24 @@ For example, the content of `[appname].runtimeconfig.json` generated from above } ``` +#### Enabling modern rendering for composite controls + +`ToolStrip`, `MenuStrip`, and `DataGridView` use Classic rendering when `VisualStylesMode` +is `Net11` or `Latest` by default. Applications can opt in to modern rendering with these +switches: + +```json +{ + "configProperties": { + "System.Windows.Forms.ToolStripModernRendering": true, + "System.Windows.Forms.DataGridViewModernRendering": true + } +} +``` + +`System.Windows.Forms.ToolStripModernRendering` controls both `ToolStrip` and `MenuStrip`. +Both switches default to `false`. + The target framework information added to `[appname].runtimeconfig.json` file always match with the application target framework irrespective of runtime/SDK installed on the machine or runtime used ([roll-forward scenarios](https://docs.microsoft.com/dotnet/core/versions/selection#framework-dependent-apps-roll-forward)) by the Windows Forms application. #### .NET runtime support in reading `runtimeconfig.json` diff --git a/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs b/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs index effd0dd4629..8634da996eb 100644 --- a/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs +++ b/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs @@ -27,6 +27,8 @@ internal static partial class LocalAppContextSwitches internal const string TreeNodeCollectionAddRangeRespectsSortOrderSwitchName = "System.Windows.Forms.TreeNodeCollectionAddRangeRespectsSortOrder"; internal const string MoveTreeViewTextLocationOnePixelSwitchName = "System.Windows.Forms.TreeView.MoveTreeViewTextLocationOnePixel"; internal const string DataGridViewDarkModeThemingSwitchName = "System.Windows.Forms.DataGridViewDarkModeTheming"; + internal const string ToolStripModernRenderingSwitchName = "System.Windows.Forms.ToolStripModernRendering"; + internal const string DataGridViewModernRenderingSwitchName = "System.Windows.Forms.DataGridViewModernRendering"; private static int s_scaleTopLevelFormMinMaxSizeForDpi; private static int s_anchorLayoutV2; @@ -41,6 +43,8 @@ internal static partial class LocalAppContextSwitches private static int s_moveTreeViewTextLocationOnePixel; private static int s_dataGridViewDarkModeTheming; + private static int s_toolStripModernRendering; + private static int s_dataGridViewModernRendering; private static FrameworkName? s_targetFrameworkName; @@ -254,4 +258,24 @@ public static bool DataGridViewDarkModeTheming [MethodImpl(MethodImplOptions.AggressiveInlining)] get => GetCachedSwitchValue(DataGridViewDarkModeThemingSwitchName, ref s_dataGridViewDarkModeTheming); } + + /// + /// Indicates whether ToolStrip controls, including MenuStrip, support modern visual styles. + /// Defaults to . + /// + public static bool ToolStripModernRendering + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => GetCachedSwitchValue(ToolStripModernRenderingSwitchName, ref s_toolStripModernRendering); + } + + /// + /// Indicates whether DataGridView controls support modern visual styles. + /// Defaults to . + /// + public static bool DataGridViewModernRendering + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => GetCachedSwitchValue(DataGridViewModernRenderingSwitchName, ref s_dataGridViewModernRendering); + } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Control.cs b/src/System.Windows.Forms/System/Windows/Forms/Control.cs index d0df1503dbd..cb67f146d3e 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Control.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Control.cs @@ -924,18 +924,30 @@ private bool ShouldSerializeVisualStylesMode() private void ResetVisualStylesMode() => VisualStylesMode = VisualStylesMode.Inherit; - private VisualStylesMode ResolvedVisualStylesMode + private VisualStylesMode UncoercedVisualStylesMode { get { VisualStylesMode value = VisualStylesMode; return value == VisualStylesMode.Inherit - ? ParentInternal?.ResolvedVisualStylesMode ?? DefaultVisualStylesMode + ? ParentInternal?.UncoercedVisualStylesMode ?? DefaultVisualStylesMode : value; } } + private VisualStylesMode ResolvedVisualStylesMode + => GetSupportedVisualStylesMode( + VisualStylesMode == VisualStylesMode.Inherit + ? ParentInternal?.ResolvedVisualStylesMode ?? DefaultVisualStylesMode + : VisualStylesMode); + + /// + /// Coerces a requested visual styles mode to one supported by this control. + /// + private protected virtual VisualStylesMode GetSupportedVisualStylesMode(VisualStylesMode mode) + => mode; + /// /// Gets the renderer-authoritative that controls must honor when deciding /// or paint behavior, after applying the High Contrast and disabled clamps. @@ -7443,20 +7455,62 @@ protected virtual void OnParentVisualStylesModeChanged(EventArgs e) return; } + VisualStylesModeChangeEventArgs? transition = e as VisualStylesModeChangeEventArgs; + if (transition is not null + && (!transition.IsCurrent + || ParentInternal?.EffectiveVisualStylesMode != transition.NewEffectiveVisualStylesMode)) + { + return; + } + if (Properties.ContainsKey(s_visualStylesModeProperty)) { - // A local value isolates this subtree from parent changes. + if (ParentInternal is not { } parent + || Properties.GetValueOrDefault(s_visualStylesModeProperty) + != parent.UncoercedVisualStylesMode + || parent.UncoercedVisualStylesMode == parent.EffectiveVisualStylesMode) + { + // A local value isolates this subtree from parent changes. + return; + } + + VisualStylesMode oldEffectiveVisualStylesMode = EffectiveVisualStylesMode; + + // Inherit the parent's coerced mode when the local value matches its uncoerced mode. + Properties.RemoveValue(s_visualStylesModeProperty); + + VisualStylesMode newEffectiveVisualStylesMode = EffectiveVisualStylesMode; + if (oldEffectiveVisualStylesMode == newEffectiveVisualStylesMode) + { + return; + } + + OnVisualStylesModeChanged( + transition?.CreateForControl( + this, + oldEffectiveVisualStylesMode, + newEffectiveVisualStylesMode) + ?? e); + return; } - if (e is VisualStylesModeChangeEventArgs transition - && (!transition.IsCurrent - || ParentInternal?.EffectiveVisualStylesMode != transition.NewEffectiveVisualStylesMode)) + if (transition is not null) { + VisualStylesModeChangeEventArgs transitionForControl = transition.CreateForControl(this); + if (transitionForControl.OldEffectiveVisualStylesMode + != transitionForControl.NewEffectiveVisualStylesMode) + { + OnVisualStylesModeChanged(transitionForControl); + } + else if (ChildControls is { } children) + { + CascadeVisualStylesModeChanged(children, transitionForControl, transitionForControl); + } + return; } - // In every other case we're going to raise the event. OnVisualStylesModeChanged(e); } @@ -7523,19 +7577,34 @@ public bool IsCurrent public VisualStylesModeChangeEventArgs CreateForControl(Control control) { - if (SystemVisualSettingsTransition is not { } systemVisualSettingsTransition) + if (SystemVisualSettingsTransition is { } systemVisualSettingsTransition) { - return this; + return new( + _state, + control, + control.Properties.GetValueOrDefault(s_visualStylesModeChangeVersionProperty, 0), + control.GetEffectiveVisualStylesMode(systemVisualSettingsTransition.OldSettings.HighContrastEnabled), + control.GetEffectiveVisualStylesMode(systemVisualSettingsTransition.NewSettings.HighContrastEnabled), + systemVisualSettingsTransition); } - return new( + return CreateForControl( + control, + control.GetSupportedVisualStylesMode(OldEffectiveVisualStylesMode), + control.GetSupportedVisualStylesMode(NewEffectiveVisualStylesMode)); + } + + public VisualStylesModeChangeEventArgs CreateForControl( + Control control, + VisualStylesMode oldEffectiveVisualStylesMode, + VisualStylesMode newEffectiveVisualStylesMode) + => new( _state, control, control.Properties.GetValueOrDefault(s_visualStylesModeChangeVersionProperty, 0), - control.GetEffectiveVisualStylesMode(systemVisualSettingsTransition.OldSettings.HighContrastEnabled), - control.GetEffectiveVisualStylesMode(systemVisualSettingsTransition.NewSettings.HighContrastEnabled), - systemVisualSettingsTransition); - } + oldEffectiveVisualStylesMode, + newEffectiveVisualStylesMode, + systemVisualSettingsTransition: null); public void PerformLayouts() { diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/DataGridView/DataGridView.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/DataGridView/DataGridView.cs index 5f1af6e83c4..1fc715f54b2 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/DataGridView/DataGridView.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/DataGridView/DataGridView.cs @@ -16,6 +16,11 @@ namespace System.Windows.Forms; [SRDescription(nameof(SR.DescriptionDataGridView))] public partial class DataGridView : Control, ISupportInitialize { + private protected override VisualStylesMode GetSupportedVisualStylesMode(VisualStylesMode mode) + => mode < VisualStylesMode.Net11 || AppContextSwitches.DataGridViewModernRendering + ? mode + : VisualStylesMode.Classic; + private static readonly object s_allowUserToAddRowsChangedEvent = new(); private static readonly object s_allowUserToDeleteRowsChangedEvent = new(); private static readonly object s_allowUserToOrderColumnsChangedEvent = new(); diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs index 6e0c698d38b..a829b6b1c51 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs @@ -23,6 +23,11 @@ namespace System.Windows.Forms; [DefaultEvent(nameof(ItemClicked))] public partial class ToolStrip : ScrollableControl, IArrangedElement, ISupportToolStripPanel { + private protected override VisualStylesMode GetSupportedVisualStylesMode(VisualStylesMode mode) + => mode < VisualStylesMode.Net11 || AppContextSwitches.ToolStripModernRendering + ? mode + : VisualStylesMode.Classic; + private static Size s_onePixel = new(1, 1); internal static Point s_invalidMouseEnter = new(int.MaxValue, int.MaxValue); diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ControlTests.VisualStylesMode.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ControlTests.VisualStylesMode.cs index 80983fcfe0b..aad305e074c 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ControlTests.VisualStylesMode.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ControlTests.VisualStylesMode.cs @@ -563,6 +563,33 @@ public void Control_VisualStylesMode_ReentrantChange_SuppressesStaleChildCascade Assert.Equal(1, childChangedCallCount); } + [WinFormsFact] + public void Control_VisualStylesMode_LocalOverrideRemovedByCoercedParent_RaisesChangedAndCascades() + { + using DataGridView parent = new() { VisualStylesMode = VisualStylesMode.Disabled }; + using SubControlWithVisualStyles child = new() + { + HighContrast = false, + VisualStylesMode = VisualStylesMode.Latest + }; + using SubControlWithVisualStyles grandchild = new() { HighContrast = false }; + child.Controls.Add(grandchild); + parent.Controls.Add(child); + + int childChangedCallCount = 0; + child.VisualStylesModeChanged += (sender, e) => childChangedCallCount++; + int grandchildChangedCallCount = 0; + grandchild.VisualStylesModeChanged += (sender, e) => grandchildChangedCallCount++; + + parent.VisualStylesMode = VisualStylesMode.Latest; + + Assert.Equal(VisualStylesMode.Inherit, child.VisualStylesMode); + Assert.Equal(VisualStylesMode.Classic, child.EffectiveVisualStylesModeAccessor); + Assert.Equal(VisualStylesMode.Classic, grandchild.EffectiveVisualStylesModeAccessor); + Assert.Equal(1, childChangedCallCount); + Assert.Equal(1, grandchildChangedCallCount); + } + [WinFormsFact] public void Appearance_ToggleSwitch_HasExpectedValue() { diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/DataGridViewTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/DataGridViewTests.cs index d79cca0216b..60d8e5baf67 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/DataGridViewTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/DataGridViewTests.cs @@ -38,6 +38,80 @@ public void DataGridView_Ctor_Default() Assert.Same(control.RowTemplate, control.RowTemplate); } + [WinFormsTheory] + [InlineData(VisualStylesMode.Net11)] + [InlineData(VisualStylesMode.Latest)] + public void DataGridView_VisualStylesMode_ModernMode_UsesClassicEffectiveMode(VisualStylesMode value) + { + using AppContextSwitchScope scope = new( + WinFormsAppContextSwitchNames.DataGridViewModernRendering, + enable: false); + using SubDataGridView control = new() { VisualStylesMode = value }; + + Assert.Equal(value, control.VisualStylesMode); + Assert.Equal(VisualStylesMode.Classic, control.EffectiveVisualStylesModeAccessor); + Assert.False(control.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData(VisualStylesMode.Net11)] + [InlineData(VisualStylesMode.Latest)] + public void DataGridView_VisualStylesMode_ModernRenderingEnabled_UsesRequestedEffectiveMode( + VisualStylesMode value) + { + using AppContextSwitchScope scope = new( + WinFormsAppContextSwitchNames.DataGridViewModernRendering, + enable: true); + using SubDataGridView control = new() { VisualStylesMode = value }; + + Assert.Equal(value, control.VisualStylesMode); + Assert.Equal(value, control.EffectiveVisualStylesModeAccessor); + Assert.False(control.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData(VisualStylesMode.Net11)] + [InlineData(VisualStylesMode.Latest)] + public void DataGridView_VisualStylesMode_InheritedModernMode_UsesClassicEffectiveMode(VisualStylesMode value) + { + using Control parent = new() { VisualStylesMode = value }; + using SubDataGridView control = new(); + parent.Controls.Add(control); + + Assert.Equal(VisualStylesMode.Inherit, control.VisualStylesMode); + Assert.Equal(VisualStylesMode.Classic, control.EffectiveVisualStylesModeAccessor); + Assert.False(control.IsHandleCreated); + } + + [WinFormsFact] + public void DataGridView_VisualStylesMode_ParentChangesToModernMode_DoesNotRaiseChanged() + { + using Control parent = new() { VisualStylesMode = VisualStylesMode.Classic }; + using SubDataGridView control = new(); + parent.Controls.Add(control); + int callCount = 0; + control.VisualStylesModeChanged += (sender, e) => callCount++; + + parent.VisualStylesMode = VisualStylesMode.Net11; + + Assert.Equal(VisualStylesMode.Classic, control.EffectiveVisualStylesModeAccessor); + Assert.Equal(0, callCount); + } + + [WinFormsFact] + public void DataGridView_VisualStylesMode_ExplicitClassic_RemainsLocalOverride() + { + using Control parent = new() { VisualStylesMode = VisualStylesMode.Net11 }; + using SubDataGridView control = new(); + parent.Controls.Add(control); + control.VisualStylesMode = VisualStylesMode.Classic; + + parent.VisualStylesMode = VisualStylesMode.Disabled; + + Assert.Equal(VisualStylesMode.Classic, control.VisualStylesMode); + Assert.Equal(VisualStylesMode.Classic, control.EffectiveVisualStylesModeAccessor); + } + private const int DefaultColumnHeadersHeight = 23; public static IEnumerable ColumnHeadersHeight_Set_TestData() @@ -2892,6 +2966,8 @@ private class SubDataGridViewCell : DataGridViewCell private class SubDataGridView : DataGridView { + public VisualStylesMode EffectiveVisualStylesModeAccessor => base.EffectiveVisualStylesMode; + public new void OnColumnHeadersHeightChanged(EventArgs e) => base.OnColumnHeadersHeightChanged(e); public new void OnColumnHeadersHeightSizeModeChanged(DataGridViewAutoSizeModeEventArgs e) => base.OnColumnHeadersHeightSizeModeChanged(e); diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ToolStripTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ToolStripTests.cs index d7f5f10b511..1fbc2806c30 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ToolStripTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ToolStripTests.cs @@ -16,6 +16,82 @@ namespace System.Windows.Forms.Tests; public partial class ToolStripTests : IDisposable { + [WinFormsTheory] + [InlineData(VisualStylesMode.Net11)] + [InlineData(VisualStylesMode.Latest)] + public void ToolStrip_VisualStylesMode_ModernMode_UsesClassicEffectiveMode(VisualStylesMode value) + { + using AppContextSwitchScope scope = new( + WinFormsAppContextSwitchNames.ToolStripModernRendering, + enable: false); + using SubToolStrip control = new() { VisualStylesMode = value }; + + Assert.Equal(value, control.VisualStylesMode); + Assert.Equal(VisualStylesMode.Classic, control.EffectiveVisualStylesModeAccessor); + Assert.False(control.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData(VisualStylesMode.Net11)] + [InlineData(VisualStylesMode.Latest)] + public void ToolStrip_VisualStylesMode_ModernRenderingEnabled_UsesRequestedEffectiveMode( + VisualStylesMode value) + { + using AppContextSwitchScope scope = new( + WinFormsAppContextSwitchNames.ToolStripModernRendering, + enable: true); + using SubToolStrip toolStrip = new() { VisualStylesMode = value }; + using SubMenuStripWithVisualStyles menuStrip = new() { VisualStylesMode = value }; + + Assert.Equal(value, toolStrip.EffectiveVisualStylesModeAccessor); + Assert.Equal(value, menuStrip.EffectiveVisualStylesModeAccessor); + Assert.False(toolStrip.IsHandleCreated); + Assert.False(menuStrip.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData(VisualStylesMode.Net11)] + [InlineData(VisualStylesMode.Latest)] + public void ToolStrip_VisualStylesMode_InheritedModernMode_UsesClassicEffectiveMode(VisualStylesMode value) + { + using Control parent = new() { VisualStylesMode = value }; + using SubToolStrip control = new(); + parent.Controls.Add(control); + + Assert.Equal(VisualStylesMode.Inherit, control.VisualStylesMode); + Assert.Equal(VisualStylesMode.Classic, control.EffectiveVisualStylesModeAccessor); + Assert.False(control.IsHandleCreated); + } + + [WinFormsFact] + public void ToolStrip_VisualStylesMode_ParentChangesToModernMode_DoesNotRaiseChanged() + { + using Control parent = new() { VisualStylesMode = VisualStylesMode.Classic }; + using SubToolStrip control = new(); + parent.Controls.Add(control); + int callCount = 0; + control.VisualStylesModeChanged += (sender, e) => callCount++; + + parent.VisualStylesMode = VisualStylesMode.Net11; + + Assert.Equal(VisualStylesMode.Classic, control.EffectiveVisualStylesModeAccessor); + Assert.Equal(0, callCount); + } + + [WinFormsFact] + public void ToolStrip_VisualStylesMode_ExplicitClassic_RemainsLocalOverride() + { + using Control parent = new() { VisualStylesMode = VisualStylesMode.Net11 }; + using SubToolStrip control = new(); + parent.Controls.Add(control); + control.VisualStylesMode = VisualStylesMode.Classic; + + parent.VisualStylesMode = VisualStylesMode.Disabled; + + Assert.Equal(VisualStylesMode.Classic, control.VisualStylesMode); + Assert.Equal(VisualStylesMode.Classic, control.EffectiveVisualStylesModeAccessor); + } + [WinFormsFact] public void ToolStrip_Ctor_Default() { @@ -7513,6 +7589,14 @@ private class SubToolStripDropDown : ToolStripDropDown public new bool ProcessDialogChar(char charCode) => base.ProcessDialogChar(charCode); } + /// + /// Exposes the effective visual styles mode for MenuStrip tests. + /// + private sealed class SubMenuStripWithVisualStyles : MenuStrip + { + public VisualStylesMode EffectiveVisualStylesModeAccessor => base.EffectiveVisualStylesMode; + } + private class SubToolStrip : ToolStrip { public new const int ScrollStateAutoScrolling = ScrollableControl.ScrollStateAutoScrolling; @@ -7537,6 +7621,8 @@ public SubToolStrip(ToolStripItem[] items) : base(items) public new bool CanRaiseEvents => base.CanRaiseEvents; + public VisualStylesMode EffectiveVisualStylesModeAccessor => base.EffectiveVisualStylesMode; + public new CreateParams CreateParams => base.CreateParams; public new Cursor DefaultCursor => base.DefaultCursor; diff --git a/src/test/util/System.Windows.Forms/WinFormsAppContextSwitchNames.cs b/src/test/util/System.Windows.Forms/WinFormsAppContextSwitchNames.cs index a9458f9b799..2eaad17db03 100644 --- a/src/test/util/System.Windows.Forms/WinFormsAppContextSwitchNames.cs +++ b/src/test/util/System.Windows.Forms/WinFormsAppContextSwitchNames.cs @@ -50,4 +50,16 @@ public const string ServicePointManagerCheckCrl /// public const string TreeNodeCollectionAddRangeRespectsSortOrder = "System.Windows.Forms.TreeNodeCollectionAddRangeRespectsSortOrder"; + + /// + /// The switch that controls whether ToolStrip controls, including MenuStrip, use modern rendering. + /// + public const string ToolStripModernRendering + = "System.Windows.Forms.ToolStripModernRendering"; + + /// + /// The switch that controls whether DataGridView controls use modern rendering. + /// + public const string DataGridViewModernRendering + = "System.Windows.Forms.DataGridViewModernRendering"; }