WPF und Per-Monitor-DPI
- Matt
Mein Notebook läuft mit 150 Prozent Skalierung, der externe Monitor daneben mit
- 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.