High DPI, accessibility and theming

Per-monitor DPI awareness, autoscaling pitfalls, screen reader names and tab order, keyboard-only operation, and the honest state of dark mode in WinForms.

Per-monitor DPI

<!-- App.manifest: opt in to per-monitor v2, not just system aware -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
  <windowsSettings>
    <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
    <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
  </windowsSettings>
</application>
public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
        AutoScaleMode = AutoScaleMode.Dpi;     // scale from the font/DPI, not pixels
        AutoScaleDimensions = new SizeF(96F, 96F);
    }

    protected override void OnDpiChangedAfterParent(EventArgs e)
    {
        base.OnDpiChangedAfterParent(e);
        // anything laid out by hand must be recomputed here
        RebuildToolbarIcons(DeviceDpi);
    }
}
  • AutoScaleMode.Font is the default and is usually right; Dpi is the honest choice when you place things programmatically.
  • Never hard-code pixel sizes and then also let autoscaling run - you get double scaling and clipped text.
  • Anchoring and docking scale correctly; absolute coordinates do not. This is the main practical reason to prefer layout panels plus Dock and Anchor.
  • DeviceDpi is the current monitor's value; LogicalToDeviceUnits converts a design-time measurement.

Accessibility essentials

// the accessible name a screen reader announces
nameBox.AccessibleName = "Customer name";
nameBox.AccessibleDescription = "The full name as it appears on the invoice";

// keep related controls in the reading order the eye sees
label.Tag = nameBox;
label.UseMnemonic = true;
label.Text = "&Customer name:";        // Alt+C focuses the box

// give the custom control a real role and a value
statusPill.AccessibleRole = AccessibleRole.StatusBar;
statusPill.AccessibleName = "Connection";

// name the form itself
Text = "Order tool";
AccessibleName = "Order tool";
ConcernControl-level settingNote
Screen reader nameAccessibleNameA label with a mnemonic is associated automatically
Tab orderTabIndexSet in reading order; TabStop = false for pure display controls
GroupingGroupBox, TabControlReal containers give a navigable structure
ZoomForm font, not a pictureText scales; images baked into a background do not
Mnemonic keys& in TextAlt plus the letter moves focus
Error messageErrorProviderIt is exposed to assistive technology, a MessageBox is not

Check three things on any form: every control reachable with Tab only, no control that steals focus back, and every icon-only button carrying an AccessibleName - a screen reader reads an unnamed button as just "button".

Theming and dark mode

WinForms has no theme system. The supported route is Application.SetColorMode on .NET 9 and later, which switches supported controls to the system dark theme; anything drawn by hand must follow it.

// Program.cs - .NET 9 and later
Application.SetColorMode(SystemColorMode.System);

// older targets: build a small palette and apply it in one pass
static void ApplyDark(Control root)
{
    root.BackColor = Color.FromArgb(32, 32, 32);
    root.ForeColor = Color.Gainsboro;
    foreach (Control child in root.Controls)
    {
        if (child is TextBoxBase or ListControl)
        {
            child.BackColor = Color.FromArgb(45, 45, 48);
            child.ForeColor = Color.Gainsboro;
        }
        ApplyDark(child);
    }
}
⚠️
If you support a dark theme you must honour the system setting on startup and on change, and check every custom-drawn control: hard-coded Color.White backgrounds are invisible in dark mode and the failure is a support report, not a compiler error.

FAQ

Should I set DPI awareness in code or the manifest?
The manifest. It is read before any window exists, so it applies to the process from the first frame and cannot be overridden by another library's configuration.
Is WinForms accessible by default?
The native controls expose proper roles once they are labelled. The gaps are custom-drawn controls, icon-only buttons and anything built from PictureBox instead of a real control - all fixable with the properties above.

Drawing, images and printing with GDI+ Deployment: ClickOnce, MSIX and single-file

Last refreshed 2026-09-18.