Per-Monitor-DPI 的 WPF 窗口启动位置

问题描述 投票:0回答:2

努力让 WPF 窗口显示在具有混合 DPI 显示器的辅助屏幕上。可在 .NET Framework 4.8 和 .NET Standard 2.0 中重现

设置:

主显示器:4K,250%

辅助显示器:1080p,100%

第1步:

添加 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>

第2步:

    public MainWindow()
    {
      SourceInitialized += (_, __) =>
      {
        WindowStartupLocation = WindowStartupLocation.Manual;
        WindowState = WindowState.Normal;

        Width = 1920;
        Height = 1050;

        Left = -1920;
        Top = 0;
      };

      InitializeComponent();
    }

结果:

MainWindow 确实显示在辅助屏幕上,但左侧/顶部错误并且使用主屏幕的 DPI。只有宽度和高度是正确的。

参考资料:

我找到的唯一参考资料是关于记事本的,是用 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 周年更新/#jwYiMyGKQRTHkBP7.97

https://github.com/Microsoft/Windows-classic-samples/tree/main/Samples/DPIAwarenessPerWindow

GitHub 上的讨论(WPF 解决方法)

https://github.com/dotnet/wpf/issues/4127

它说的是有关 SetThreadDpiAwarenessContext 的内容,但我不清楚如何使其在 C# 中工作......

DPI_AWARENESS_CONTEXT previousDpiContext = 
SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);
BOOL ret = SetWindowPlacement(hwnd, wp);
SetThreadDpiAwarenessContext(previousDpiContext);
c# wpf dpi
2个回答
0
投票

您可以将窗口移动到任何显示器的中心。这只是一个计算问题。

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;

public static class WindowHelper
{
    public static void MoveToCenter(Window window)
    {
        if (!GetCursorPos(out POINT cursorPoint))
            return;

        IntPtr monitorHandle = MonitorFromPoint(cursorPoint, MONITOR_DEFAULTTO.MONITOR_DEFAULTTONULL);

        MONITORINFO monitorInfo = new() { cbSize = (uint)Marshal.SizeOf<MONITORINFO>() };
        if (!GetMonitorInfo(monitorHandle, ref monitorInfo))
            return;

        IntPtr windowHandle = new WindowInteropHelper(window).EnsureHandle();

        if (!GetWindowPlacement(windowHandle, out WINDOWPLACEMENT windowPlacement))
            return;

        int left = monitorInfo.rcWork.left + Math.Max(0, (int)((monitorInfo.rcWork.Width - windowPlacement.rcNormalPosition.Width) / 2D));
        int top = monitorInfo.rcWork.top + Math.Max(0, (int)((monitorInfo.rcWork.Height - windowPlacement.rcNormalPosition.Height) / 2D));

        windowPlacement.rcNormalPosition = new RECT(left, top, windowPlacement.rcNormalPosition.Width, windowPlacement.rcNormalPosition.Height);
        SetWindowPlacement(windowHandle, ref windowPlacement);
    }

    [DllImport("User32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GetCursorPos(out POINT lpPoint);

    [DllImport("User32.dll")]
    private static extern IntPtr MonitorFromPoint(POINT pt, MONITOR_DEFAULTTO dwFlags);

    private enum MONITOR_DEFAULTTO : uint
    {
        MONITOR_DEFAULTTONULL = 0x00000000,
        MONITOR_DEFAULTTOPRIMARY = 0x00000001,
        MONITOR_DEFAULTTONEAREST = 0x00000002,
    }

    [DllImport("User32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);

    [StructLayout(LayoutKind.Sequential)]
    private struct MONITORINFO
    {
        public uint cbSize;
        public RECT rcMonitor;
        public RECT rcWork;
        public uint dwFlags;
    }

    [DllImport("User32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GetWindowPlacement(IntPtr hWnd, out WINDOWPLACEMENT lpwndpl);

    [DllImport("User32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetWindowPlacement(IntPtr hWnd, [In] ref WINDOWPLACEMENT lpwndpl);

    [StructLayout(LayoutKind.Sequential)]
    private struct WINDOWPLACEMENT
    {
        public uint length;
        public uint flags;
        public uint showCmd;
        public POINT ptMinPosition;
        public POINT ptMaxPosition;
        public RECT rcNormalPosition;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct POINT
    {
        public int x;
        public int y;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct RECT
    {
        public int left;
        public int top;
        public int right;
        public int bottom;

        public int Width => right - left;
        public int Height => bottom - top;

        public RECT(int x, int y, int width, int height)
        {
            left = x;
            top = y;
            right = x + width;
            bottom = y + height;
        }
    }
}
using System.Windows;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        WindowStartupLocation = WindowStartupLocation.CenterScreen;
        InitializeComponent();        
    }

    private bool _isMoved;

    protected override Size ArrangeOverride(Size arrangeBounds)
    {
        if (!_isMoved)
        {
            _isMoved = true;
            WindowHelper.MoveToCenter(this);
        }
        return base.ArrangeOverride(arrangeBounds);
    }
}

但我发现标题栏的 DPI 与主显示器的 DPI 保持一致。一般来说,非客户区的DPI是很难修复的。所以这个 hack 不太实用,除非删除默认标题栏。


0
投票

根据这篇文章,我设法找到了解决方案: https://learn.microsoft.com/en-us/office/client-developer/ddpi/handle-high-dpi-and-dpi-scaling-in-your-office-solution

这个想法是在创建对话框期间将线程的 DPI 感知上下文设置为 UNAWARE。



    /// 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(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;
    
        }
    
    }

使用:


    var window = ThreadDpiAwarenessContext.CreateWindow(factory: () =>
        {
            var result = new MainWindow()
            {
                Top = 500,
                Left = 500
            };
            return result;
        });
          
    window.Show();
© www.soinside.com 2019 - 2024. All rights reserved.