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
| Mode | Behavior |
|---|---|
| Unaware | System stretches your bitmap. Blurry on high-DPI, always. |
| System-aware | One scale for the whole session (the monitor Windows is “on” at logon). Cross-monitor drag looks wrong. |
| Per-Monitor V2 | Each 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
- Which runtime?
dotnet --version/ check the target framework. .NET Framework 4.x needs the manifest above; modern .NET usually doesn’t. - 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.
- 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 (useImageSourcepack URIs per DPI:Images/icon_@2x.png) or use vector (SVG→Geometry, or a font icon).BitmapCache/VisualBrushsnapshots 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
- WPF DPI awareness — the three awareness modes this page’s fix targets.
- High-DPI support in Windows — OS-side behavior and manifest-based configuration.