WPF und Per-Monitor-DPI

WPF und Per-Monitor-DPI

- Matt

Mein Notebook läuft mit 150 Prozent Skalierung, der externe Monitor daneben mit

  1. Sobald ich ein WPF-Fenster vom einen auf den anderen Bildschirm ziehe, werden Icons unscharf und Abstände stimmen nicht mehr. Das Fenster selbst wird korrekt neu positioniert, aber der Inhalt wirkt wie hochskaliertes Pixelmaterial. Der Grund: WPF-Anwendungen sind standardmäßig nur system-DPI-aware, nicht per-monitor-aware. Windows übernimmt beim Verschieben die Skalierung selbst, und genau das sieht man dem Ergebnis an.

Das Manifest ist der eigentliche Schalter

Die Lösung liegt nicht im Code, sondern im Anwendungsmanifest. Ein app.manifest mit folgendem Eintrag schaltet PerMonitorV2 ein:

<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</dpiAware>
  </windowsSettings>
</application>

Im Projekt muss die Datei zusätzlich eingebunden werden:

<PropertyGroup>
  <ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>

Das dpiAware-Element bleibt als Fallback für ältere Windows-Versionen stehen, die PerMonitorV2 noch nicht kennen. Microsoft empfiehlt ausdrücklich den Weg über das Manifest statt über SetProcessDpiAwarenessContext im Code, weil die Einstellung dann feststeht, bevor überhaupt Anwendungscode läuft.

Die Falle beim Multi-Targeting

Was in keiner Doku steht, aber im WPF-Repository als Issue #7609 dokumentiert ist: Baut man mit dem .NET-7-SDK gegen net6.0-windows, landet trotz korrektem Manifest nur System-DPI-Awareness im fertigen Executable. Zielt dasselbe Projekt auf net7.0-windows, funktioniert PerMonitorV2 wie erwartet. Wer ein älteres Projekt nur auf ein neueres SDK hebt, ohne das Target Framework mitzuziehen, wundert sich sonst, warum das Manifest scheinbar wirkungslos bleibt.

Was WPF danach selbst erledigt und was nicht

Ist PerMonitorV2 aktiv, reagiert reiner WPF-Inhalt von selbst richtig: Layout, Text und Vektorgrafik werden für die tatsächliche Monitor-DPI neu berechnet, nicht nachträglich hochskaliert. Für Fensterlogik, die auf DPI-Wechsel reagieren soll, gibt es die virtuelle Methode Window.OnDpiChanged(DpiScale oldDpi, DpiScale newDpi), und VisualTreeHelper.GetDpi(Visual) liefert die aktuell gültige Skalierung für einen beliebigen Visual.

Bitmaps profitieren davon nicht automatisch. Ein PNG-Icon, das für 96 DPI gerendert wurde, wird weiterhin von WPF hochskaliert und bleibt unscharf, wenn man nicht selbst im DpiChanged-Handler ein passendes Asset nachlädt. Noch enger wird es bei gehosteten Fremdinhalten: Ein WindowsFormsHost oder ein per HwndHost eingebettetes natives Fenster bekommt laut den Per-Monitor-DPI-Beispielen im WPF-Samples-Repository gar keine per-monitor-Behandlung. Wer solche Hosts einsetzt, bleibt beim alten, teils unscharfen Verhalten, egal was im Manifest steht.

Wer ein bestehendes Projekt ohnehin gerade auf ein neueres .NET hebt, kann das Manifest bei der Gelegenheit gleich mit einziehen, siehe dazu auch mein Beitrag zum Umstieg eines alten WPF-Projekts auf .NET 10.