Struggling to get a WPF Window showing up on secondary screen with mixed DPI monitors. Reproducible in .NET Framework 4.8 as well as .NET Standard 2.0
Setup:
Primary monitor : 4K, 250%
Secondary monitor: 1080p, 100%
Step 1:
add a Manifest for PerMonitorV2
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
</windowsSettings>
</application>
</assembly>
Step 2:
public MainWindow()
{
SourceInitialized += (_, __) =>
{
WindowStartupLocation = WindowStartupLocation.Manual;
WindowState = WindowState.Normal;
Width = 1920;
Height = 1050;
Left = -1920;
Top = 0;
};
InitializeComponent();
}
Result:
MainWindow is indeed showing up on secondary screen, but with wrong Left/Top and using DPI of the Primary screen. Only Width and Height are correct.
References:
The only references that I found are with regards to Notepad, are written in MFC:
https://github.com/Microsoft/Windows-classic-samples/tree/main/Samples/DPIAwarenessPerWindow
Discussion on GitHub (WPF workarounds)
https://github.com/dotnet/wpf/issues/4127
It is saying something about SetThreadDpiAwarenessContext but it is unclear to me how to make it work in C#....
DPI_AWARENESS_CONTEXT previousDpiContext =
SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);
BOOL ret = SetWindowPlacement(hwnd, wp);
SetThreadDpiAwarenessContext(previousDpiContext);
Based on this article, I managed to work out a solution: https://learn.microsoft.com/en-us/office/client-developer/ddpi/handle-high-dpi-and-dpi-scaling-in-your-office-solution
The idea is to set the Thread's DPI Awareness Context to UNAWARE, during the creation of the dialog.
/// changes the current Thread's "ThreadDpiAwarenessContext"
/// restores the current Thread's "ThreadDpiAwarenessContext" when disposed
public class ThreadDpiAwarenessContext : IDisposable
{
/// creates a Window
/// in the functor, the Window's Top, Left, Width and Height properties can be adjusted in DPI unaware space
/// i.e. these properties will be expressed in Pixels, as if every monitor has 100% zoom factor
public static T CreateWindow<T>(Func factory) where T : Window
{
using (new ThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT.DPI_AWARENESS_CONTEXT_UNAWARE))
{
return factory();
}
}
/// changes the current Thread's "ThreadDpiAwarenessContext"
/// restores the current Thread's "ThreadDpiAwarenessContext" when disposed
public ThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT contextSwitchTo)
{
_resetContext = SetThreadDpiAwarenessContext(contextSwitchTo);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
SetThreadDpiAwarenessContext(_resetContext);
}
_disposed = true;
}
private DPI_AWARENESS_CONTEXT _resetContext;
private bool _disposed = false;
[DllImport("user32.dll")]
private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
public struct DPI_AWARENESS_CONTEXT
{
public static readonly DPI_AWARENESS_CONTEXT DPI_AWARENESS_CONTEXT_INVALID = IntPtr.Zero;
public static readonly DPI_AWARENESS_CONTEXT DPI_AWARENESS_CONTEXT_UNAWARE = new IntPtr(-1);
public static readonly DPI_AWARENESS_CONTEXT DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = new IntPtr(-2);
public static readonly DPI_AWARENESS_CONTEXT DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = new IntPtr(-3);
public static readonly DPI_AWARENESS_CONTEXT DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = new IntPtr(-4);
private DPI_AWARENESS_CONTEXT(IntPtr value) => _value = value;
public static implicit operator DPI_AWARENESS_CONTEXT(IntPtr value) => new DPI_AWARENESS_CONTEXT(value);
public static implicit operator IntPtr(DPI_AWARENESS_CONTEXT context) => context._value;
public static bool operator ==(IntPtr context1, DPI_AWARENESS_CONTEXT context2) => AreDpiAwarenessContextsEqual(context1, context2);
public static bool operator !=(IntPtr context1, DPI_AWARENESS_CONTEXT context2) => (context1 == context2) == false;
public override bool Equals(object obj) => base.Equals(obj);
public override int GetHashCode() => base.GetHashCode();
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AreDpiAwarenessContextsEqual(IntPtr dpiContextA, IntPtr dpiContextB);
private IntPtr _value;
}
}
to use:
var window = ThreadDpiAwarenessContext.CreateWindow(
factory: () => new MainWindow()
{
Top = 500,
Left = 500
});
window.Show();