努力让 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://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);
您可以将窗口移动到任何显示器的中心。这只是一个计算问题。
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 不太实用,除非删除默认标题栏。
根据这篇文章,我设法找到了解决方案: 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();