Software & Development

Fix Blurry WPF Windows on 4K and Mixed-DPI Monitors

WPF app looks blurred after moving between a laptop panel and a 4K display? Set Per-Monitor-V2 DPI awareness correctly instead of fighting the scaling slider.

Symptoms: a WPF window is crisp on one monitor, then — after dragging it to the 4K display or changing the scale factor — text turns soft and bitmap controls look smudged. Windows has been scaling your window for you, and the result is a rasterized upscaled version of a low-DPI render.

The three DPI awareness modes, briefly

ModeBehavior
UnawareSystem stretches your bitmap. Blurry on high-DPI, always.
System-awareOne scale for the whole session (the monitor Windows is “on” at logon). Cross-monitor drag looks wrong.
Per-Monitor V2Each window uses its monitor’s scale; WPF re-renders on change. What you want.

.NET 3.0+ WPF runs PerMonitorV2 out of the box. So diagnose in this order:

Diagnose first

  1. Which runtime? dotnet --version / check the target framework. .NET Framework 4.x needs the manifest above; modern .NET usually doesn’t.
  2. Vector or bitmap? Zoom into the blurred region. If everything is soft, it’s a DPI awareness or caching issue. If only images/icons are soft while text is crisp, you’re stretching bitmaps — see the next section.
  3. Reproduce deterministically: note the scale factors (Settings → Display) of both monitors. A 100% laptop + 250% 4K panel is the classic failure pair.

Bitmaps: the usual suspect on modern .NET

Text and vector shapes scale for free in WPF. Raster content does not. Common offenders:

  • <Image> with a small source bitmap displayed large — ship assets at multiple resolutions (use ImageSource pack URIs per DPI: Images/icon_@2x.png) or use vector (SVG→Geometry, or a font icon).
  • BitmapCache / VisualBrush snapshots taken at one DPI and reused on another. Invalidate or cache per-DPI.
  • Toolbars/toolkits rendering icons via GDI+ interop — those need explicit ScaleTransforms or DPI-aware sources.
// Re-render a cached visual when the DPI changes instead of stretching it.
visual.AddHandler(
    FrameworkElement.DpiChangedEvent,
    new DpiChangedEventHandler((_, e) => RebuildCacheFor(e.AfterDpi)));

Verify awareness at runtime

using System.Windows;
using System.Windows.Interop;

var src = HwndSource.FromHwnd(new WindowInteropHelper(window).HandleHwnd);
Console.WriteLine(src?.CompositionTarget?.TransformToDevice.M11); // 1.0 @100%, 2.5 @250%

If this prints 1.0 on a 250% monitor, the process is not getting PerMonitorV2 treatment — revisit the manifest or startup context.

Common problems

“The manifest made my window tiny / doubled”

PerMonitorV2 changes the unit WPF measures in at high scale: some apps that hard-coded pixel sizes suddenly look half-size. Replace fixed pixel dimensions with DeviceIndependentUnit-friendly values (WPF units already are — check you aren’t mixing in raw pixels from GDI interop).

Works standalone, blurry when launched from a tray/32-bit host

DPI awareness is per process, inherited from the creating process. If a 32-bit or unaware shell starts your app, you inherit its limits. Launch your app as its own top-level process.

UWP/WinUI hosts, remote desktop, or virtual machines

RDP and some VM sessions force System DPI for the session; PerMonitorV2 cannot override the session policy. Test on a physical multi-monitor setup before debugging this further.

Sources and upstream documentation