Search code examples
c#wpfdpi

WPF Window startup location for Per-Monitor-DPI


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%

enter image description here

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://blogs.windows.com/windowsdeveloper/2016/10/24/high-dpi-scaling-improvements-for-desktop-applications-and-mixed-mode-dpi-scaling-in-the-windows-10-anniversary-update/#jwYiMyGKQRTHkBP7.97

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);

Solution

  • 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();