Skip to content

[NET 11 API Proposal] Application.SystemVisualSettings — accent color, text scale, and accessibility visual settings #14583

Description

@KlausLoeffelmann

Background and motivation

This proposal adds a consolidated Application.SystemVisualSettings snapshot plus one change
notification, covering the Windows accent color, the Accessibility Text size factor, high
contrast, client-area animation, keyboard-cue visibility, and focus-border metrics.

This design evolved during implementation. It originally started as two narrow, single-purpose
members — Application.GetWindowsAccentColor() and Application.SystemTextSize /
SystemTextSizeChanged (plus a mirrored Form.SystemTextSizeChanged). While implementing those,
it became clear that Windows reports visual/accessibility changes through four different messages
(WM_SETTINGCHANGE, WM_DWMCOLORIZATIONCOLORCHANGED, WM_THEMECHANGED, WM_SYSCOLORCHANGE), that
several other related settings needed the same treatment, and that a static-event-only design risks
the classic leak of rooting every subscriber for the application's lifetime. The proposal below is
the result: one immutable settings snapshot and one leak-free notification path.

Image

SystemVisualSettings.TextScaleFactor represents Settings > Accessibility > Text size, not
display DPI scaling. DPI remains covered by HighDpiMode, WM_DPICHANGED, and the existing DPI
events.

Note also: Animation can also be controlled via the Windows A11Y settings, and using VisualStylesMode controlled styles are including a bunch of different Animations:

Image Image

API proposal

namespace System.Windows.Forms;

/// <summary>
///  Represents an immutable snapshot of visual and accessibility settings that Windows owns.
/// </summary>
public sealed class SystemVisualSettings
{
    /// <summary>Gets the user's current Windows accent color.</summary>
    public Color AccentColor { get; }

    /// <summary>
    ///  Gets the Windows Accessibility text-scale factor (1.0-2.25), independent of display DPI.
    /// </summary>
    public float TextScaleFactor { get; }

    /// <summary>Gets whether the user has enabled Windows high-contrast mode.</summary>
    public bool HighContrastEnabled { get; }

    /// <summary>Gets whether Windows enables client-area animations.</summary>
    public bool ClientAreaAnimationEnabled { get; }

    /// <summary>Gets whether the system default displays keyboard cues.</summary>
    public bool KeyboardCuesVisible { get; }

    /// <summary>Gets the Windows focus-border width and height, in pixels.</summary>
    public Size FocusBorderMetrics { get; }
}

[Flags]
public enum SystemVisualSettingsCategories
{
    None = 0,
    AccentColor = 1 << 0,
    TextScale = 1 << 1,
    HighContrast = 1 << 2,
    Animations = 1 << 3,
    KeyboardCues = 1 << 4,
    FocusMetrics = 1 << 5
}

public class SystemVisualSettingsChangedEventArgs : EventArgs
{
    public SystemVisualSettings OldSettings { get; }
    public SystemVisualSettings NewSettings { get; }
    public SystemVisualSettingsCategories Changed { get; }
}

public delegate void SystemVisualSettingsChangedEventHandler(object? sender, SystemVisualSettingsChangedEventArgs e);

public sealed partial class Application
{
    /// <summary>Gets the current Windows visual and accessibility settings snapshot.</summary>
    public static SystemVisualSettings SystemVisualSettings { get; }

    /// <summary>
    ///  Occurs when Windows visual or accessibility settings change. Intended for
    ///  application-lifetime consumers (theming engines, services); subscribers must
    ///  unsubscribe if their lifetime is shorter than the application. Controls and forms
    ///  should use <see cref="Control.SystemVisualSettingsChanged"/> instead.
    /// </summary>
    public static event SystemVisualSettingsChangedEventHandler? SystemVisualSettingsChanged;
}

public partial class Control
{
    /// <summary>
    ///  Occurs when Windows visual or accessibility settings change. Unlike
    ///  <see cref="Application.SystemVisualSettingsChanged"/>, this instance event requires no
    ///  unsubscription — it follows the control's own lifetime.
    /// </summary>
    public event SystemVisualSettingsChangedEventHandler? SystemVisualSettingsChanged;

    /// <summary>Raises the <see cref="SystemVisualSettingsChanged"/> event.</summary>
    protected virtual void OnSystemVisualSettingsChanged(SystemVisualSettingsChangedEventArgs e);
}

Usage

// App-level, e.g. a theming service:
Color accent = Application.SystemVisualSettings.AccentColor;
Application.SystemVisualSettingsChanged += (s, e) =>
{
    if (e.Changed.HasFlag(SystemVisualSettingsCategories.AccentColor))
    {
        // Re-theme using e.NewSettings.AccentColor.
    }
};
// Control/Form-level, no unsubscription needed:
public sealed class MainForm : Form
{
    private const float BaseFontSize = 9.0f;

    protected override void OnSystemVisualSettingsChanged(SystemVisualSettingsChangedEventArgs e)
    {
        base.OnSystemVisualSettingsChanged(e);

        if (e.Changed.HasFlag(SystemVisualSettingsCategories.TextScale))
        {
            Font = new Font(Font.FontFamily, BaseFontSize * e.NewSettings.TextScaleFactor, Font.Style, Font.Unit);
            PerformLayout();
        }
    }
}

Applications that replace fonts repeatedly should retain and dispose the font instances they own.
The framework deliberately does not perform automatic font scaling or relayout — font and layout
policies are application-specific.

Rationale and alternative designs

  • One snapshot type instead of N separate getters/events: accent color, text scale, high
    contrast, animations, keyboard cues, and focus-border metrics all arrive through the same set of
    Windows messages; one type with a flags-based Changed lets consumers early-out cheaply instead of
    wiring up several independent events.
  • Control instance event/virtual instead of relying only on the static Application event: a
    static-only design requires every subscribing control/form to unsubscribe or leak; the Control
    cascade (parent → children, modeled on the existing OnSystemColorsChanged pattern) is scoped to
    the control's own lifetime and needs no subscription management.
  • Flags enum for Changed instead of separate events per category: keeps the notification
    surface to one event while still letting consumers react selectively.
  • Considered and rejected: keeping the original narrow GetWindowsAccentColor() method and a
    separate SystemTextSize/SystemTextSizeChanged pair — this would have required a second,
    near-identical mechanism per additional setting (high contrast, animations, etc.) and duplicated
    the message-normalization logic.

Behavior

  • The snapshot is refreshed by normalizing WM_SETTINGCHANGE, WM_DWMCOLORIZATIONCOLORCHANGED,
    WM_THEMECHANGED, and WM_SYSCOLORCHANGE; SystemVisualSettingsChanged is raised once per actual
    transition, with Changed reporting only the categories that differ from the previous snapshot.
  • Applications opt in simply by subscribing to an event or overriding
    Control.OnSystemVisualSettingsChanged; existing applications do not change behavior.

Will this feature affect UI controls?

The API does not automatically change existing controls. It provides the shared settings snapshot
and change notification that renderers, applications, and follow-up control APIs use to update their
own rendering, measurement, and layout.

Designer impact: Control.SystemVisualSettingsChanged appears as a standard event. There are no
new serialization requirements.

Accessibility impact: Applications and controls can react when the user changes Windows
accent color, high contrast, text size, animation, or keyboard-cue settings while running.

Localization: No user-visible strings are introduced.


I'll attach an animated GIF demo later.

Metadata

Metadata

Labels

NewApi-Net11Tracks issues for public APIs targeted for .NET 11.api-approved(4) API was approved in API review, it can be implementedneeds-area-labeltenet-accessibilityMAS violation, UIA issue; problems with accessibility standards

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions