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.Fontis the default and is usually right;Dpiis 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.
DeviceDpiis the current monitor's value;LogicalToDeviceUnitsconverts 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";| Concern | Control-level setting | Note |
|---|---|---|
| Screen reader name | AccessibleName | A label with a mnemonic is associated automatically |
| Tab order | TabIndex | Set in reading order; TabStop = false for pure display controls |
| Grouping | GroupBox, TabControl | Real containers give a navigable structure |
| Zoom | Form font, not a picture | Text scales; images baked into a background do not |
| Mnemonic keys | & in Text | Alt plus the letter moves focus |
| Error message | ErrorProvider | It 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.Related
Drawing, images and printing with GDI+ Deployment: ClickOnce, MSIX and single-file
Last refreshed 2026-09-18.