缩放一组多边形以监视 WPF 中的分辨率
Scaling a set of polygons to monitor resolution in WPF
我有一个非 DPI 感知 WPF 应用程序,我想在其中绘制一组无边框的多边形 window 以完全适合显示器。我有一个算法可以根据任何给定的分辨率缩放和绘制多边形。在我的设置中,我有一个 4K 显示器和一个全高清显示器并排放置。我的 4K 显示器的比例设置为 150%,全高清显示器设置为 100%。对于 4K 显示器,这意味着如果我将 windows 宽度和高度设置为 3840x2160,则实际渲染分辨率为 2560x1440。现在,如果我将我的多边形集缩放到 4K,多边形将在 canvas 和 window 之外呈现。我怀疑这是因为多边形不知道我的 4K 显示器的 DPI 设置。如果我在我的 FullHD 显示器上绘制多边形,它们非常适合,因为显示器比例设置为 100%。
为了解决这个问题,我尝试了以下方法:
- 检索每个显示器的 DPI 并根据 DPI 缩放多边形。
部分有效。由于我的应用程序是非 DPI 感知的(请注意,我不愿意让它感知 DPI,因为这会引入一组全新的问题),任何检索显示器 DPI 的方法都会导致两个显示器都获得 144 (150%)。这导致多边形非常适合我的 4K 显示器,但它们在我的全高清显示器上会缩放得太小。我尝试了以下检索 DPI 的方法:GetDpiForMonitor
来自 Shcore.dll
、VisualTreeHelper
和 Matrixes。请注意,如果我将我的应用程序设置为 DPI 感知,这些方法确实有效,但我不能为引入的所有额外工作这样做。
- ViewBox 环绕 Canvas
当我将 canvas 宽度和高度设置为 3840x2160 时,ViewBox 不会自动缩小内容(ViewBox 要求其内容 canvas 具有设置的宽度和高度)。
- 正在检索显示器的 "real"/缩放分辨率
我的意思是我需要访问某种 API 类型的 return 我的 4K 显示器的分辨率为 2560x1440。我试过经典的 Windows.Forms.Screen
API 以及较新的 WindowsDispalyAPI。但是对于我的 4K 显示器,两者总是 return 4K 分辨率。
所以我所有的算法都有效,我只需要找到以下任何一个:
- 一种可靠的方法来检索单个显示器的 DPI,同时保持我的应用程序无 DPI 感知。
- 一种检索显示器缩放分辨率的方法。
- 缩放一组多边形以适合屏幕的其他方法。
感谢任何帮助。
编辑:
这是一个 xaml 无边框 window 示例,它在缩放比例为 150% 的 4K 屏幕上重现了该问题:
<Window x:Class="Test.Views.FullscreenPolygon"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" Width="3840" Height="2160"
WindowStyle="None" AllowsTransparency="True" Background="Transparent">
<Grid>
<Canvas x:Name="CanvasArea">
<Polygon Points="2888,0 3360,2140 3840,0" Fill="Black"></Polygon>
<Polygon Points="1920,20 1450,2160 2400,2160" Fill="Black"></Polygon>
</Canvas>
</Grid>
</Window>
如您所见,两个多边形(三角形)都经过缩放以适应 window 的 4K 分辨率。 window 本身呈现为 2560x1440,因为显示器缩放比例为 150%。然而,多边形在其外部呈现,部分呈现在我的第二个屏幕上。
编辑2:
感谢 Jeff,使用他的 project.
中的 GetScreenScaleFactorNonDpiAware
方法让它工作
我有时需要考虑屏幕缩放,正如 AlwaysLearning 指出的那样,我不得不导入和使用 user32.dll
,因为我也使用 4K 显示器,但我的显示器缩放到 125%。我为此创建了一个单独的 class。
我有一个测试程序可以利用这个 class。这是测试输出:
monitor name| \.\DISPLAY2
native dpi| 96
screen dpi| 120
scale factor| 1.25
scaling factor| 0.8
native screen size| {X=0,Y=0,Width=3840,Height=2160}
scaled screen size| {X=0,Y=0,Width=3072,Height=1728}
以上是从这里创建的:
logMsgLn2("monitor name", ScreenParameters.GetMonitorName(this));
logMsgLn2("native dpi", ScreenParameters.GetNativeScreenDpi);
logMsgLn2("screen dpi", ScreenParameters.GetScreenDpi(this));
logMsgLn2("scale factor", ScreenParameters.GetScreenScaleFactor(this));
logMsgLn2("scaling factor", ScreenParameters.GetScreenScalingFactor(this));
logMsgLn2("native screen size", ScreenParameters.GetNativeScreenSize(this));
logMsgLn2("scaled screen size", ScreenParameters.GetScaledScreenSize(this));
这是全部class:
public class ScreenParameters
{
private const double NativeScreenDpi = 96.0;
private const int CCHDEVICENAME = 32;
// private method to get the handle of the window
// this keeps this class contained / not dependant
public static double GetNativeScreenDpi
{
get => (int) NativeScreenDpi;
}
public static string GetMonitorName(Window win)
{
MONITORINFOEX mi = GetMonitorInfo(GetWindowHandle(win));
return mi.DeviceName;
}
private static IntPtr GetWindowHandle(Window win)
{
return new WindowInteropHelper(win).Handle;
}
// the actual screen DPI adjusted for the scaling factor
public static double GetScreenDpi(Window win)
{
return GetDpiForWindow(GetWindowHandle(win));
}
// this is the ratio of the current screen Dpi
// and the base Dpi
public static double GetScreenScaleFactor(Window win)
{
return (GetScreenDpi(win) / NativeScreenDpi);
}
// this is the conversion factor between screen coordinates
// and sizes and their actual actual coordinate and size
// e.g. for a screen set to 125%, this factor applied
// to the native screen dimensions, will provide the
// actual screen dimensions
public static double GetScreenScalingFactor(Window win)
{
return (1 / (GetScreenDpi(win) / NativeScreenDpi));
}
// get the dimensions of the physical / native screen
// ignoring any applied scaling
public static Rectangle GetNativeScreenSize(Window win)
{
MONITORINFOEX mi = GetMonitorInfo(GetWindowHandle(win));
return ConvertRectToRectangle(mi.rcMonitor);
}
// get the screen dimensions taking the screen scaling into account
public static Rectangle GetScaledScreenSize2(Window win)
{
double ScalingFactor = GetScreenScalingFactor(win);
Rectangle rc = GetNativeScreenSize(win);
if (ScalingFactor == 1) return rc;
return rc.Scale(ScalingFactor);
}
public static Rectangle GetScaledScreenSize(Window win)
{
double dpi = GetScreenDpi(win);
Rectangle rc = GetNativeScreenSize(win);
return ScaleForDpi(rc, dpi);
}
internal static MONITORINFOEX GetMonitorInfo(IntPtr ptr)
{
IntPtr hMonitor = MonitorFromWindow(ptr, 0);
MONITORINFOEX mi = new MONITORINFOEX();
mi.Init();
GetMonitorInfo(hMonitor, ref mi);
return mi;
}
#region + Utility methods
public static Rectangle ConvertRectToRectangle(RECT rc)
{
return new Rectangle(rc.Top, rc.Left,
rc.Right - rc.Left, rc.Bottom - rc.Top);
}
public static System.Drawing.Point ScaleForDpi(System.Drawing.Point pt, double dpi)
{
double factor = NativeScreenDpi / dpi;
return new System.Drawing.Point((int) (pt.X * factor), (int) (pt.Y * factor));
}
public static Point ScaleForDpi(Point pt, double dpi)
{
double factor = NativeScreenDpi / dpi;
return new Point(pt.X * factor, pt.Y * factor);
}
public static Size ScaleForDpi(Size size, double dpi)
{
double factor = NativeScreenDpi / dpi;
return new Size(size.Width * factor, size.Height * factor);
}
public static System.Drawing.Size ScaleForDpi(System.Drawing.Size size, double dpi)
{
double factor = NativeScreenDpi / dpi;
return new System.Drawing.Size((int) (size.Width * factor), (int) (size.Height * factor));
}
public static Rectangle ScaleForDpi(Rectangle rc, double dpi)
{
double factor = NativeScreenDpi / dpi;
return new Rectangle(ScaleForDpi(rc.Location, dpi),
ScaleForDpi(rc.Size, dpi));
}
#endregion
#region + Dll Imports
[DllImport("user32.dll")]
internal static extern UInt16 GetDpiForWindow(IntPtr hwnd);
[DllImport("user32.dll")]
internal static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
internal static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi);
[DllImport("user32.dll")]
internal static extern UInt16 GetProcessDpiAwareness(IntPtr hwnd);
#endregion
#region + Dll Enums
internal enum dwFlags : uint
{
MONITORINFO_PRIMARY = 1
}
#endregion
#region + Dll Structs
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
internal struct MONITORINFOEX
{
public uint cbSize;
public RECT rcMonitor;
public RECT rcWorkArea;
public dwFlags Flags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHDEVICENAME)]
public string DeviceName;
public void Init()
{
this.cbSize = 40 + 2 * CCHDEVICENAME;
this.DeviceName = String.Empty;
}
}
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
#endregion
}
希望对您有所帮助。
为了将来参考,我稍微更新了代码以允许非 DPI 感知监视器。我将更新后的代码放在这里 ScreenParameters
我有一个非 DPI 感知 WPF 应用程序,我想在其中绘制一组无边框的多边形 window 以完全适合显示器。我有一个算法可以根据任何给定的分辨率缩放和绘制多边形。在我的设置中,我有一个 4K 显示器和一个全高清显示器并排放置。我的 4K 显示器的比例设置为 150%,全高清显示器设置为 100%。对于 4K 显示器,这意味着如果我将 windows 宽度和高度设置为 3840x2160,则实际渲染分辨率为 2560x1440。现在,如果我将我的多边形集缩放到 4K,多边形将在 canvas 和 window 之外呈现。我怀疑这是因为多边形不知道我的 4K 显示器的 DPI 设置。如果我在我的 FullHD 显示器上绘制多边形,它们非常适合,因为显示器比例设置为 100%。
为了解决这个问题,我尝试了以下方法:
- 检索每个显示器的 DPI 并根据 DPI 缩放多边形。
部分有效。由于我的应用程序是非 DPI 感知的(请注意,我不愿意让它感知 DPI,因为这会引入一组全新的问题),任何检索显示器 DPI 的方法都会导致两个显示器都获得 144 (150%)。这导致多边形非常适合我的 4K 显示器,但它们在我的全高清显示器上会缩放得太小。我尝试了以下检索 DPI 的方法:GetDpiForMonitor
来自 Shcore.dll
、VisualTreeHelper
和 Matrixes。请注意,如果我将我的应用程序设置为 DPI 感知,这些方法确实有效,但我不能为引入的所有额外工作这样做。
- ViewBox 环绕 Canvas
当我将 canvas 宽度和高度设置为 3840x2160 时,ViewBox 不会自动缩小内容(ViewBox 要求其内容 canvas 具有设置的宽度和高度)。
- 正在检索显示器的 "real"/缩放分辨率
我的意思是我需要访问某种 API 类型的 return 我的 4K 显示器的分辨率为 2560x1440。我试过经典的 Windows.Forms.Screen
API 以及较新的 WindowsDispalyAPI。但是对于我的 4K 显示器,两者总是 return 4K 分辨率。
所以我所有的算法都有效,我只需要找到以下任何一个:
- 一种可靠的方法来检索单个显示器的 DPI,同时保持我的应用程序无 DPI 感知。
- 一种检索显示器缩放分辨率的方法。
- 缩放一组多边形以适合屏幕的其他方法。
感谢任何帮助。
编辑:
这是一个 xaml 无边框 window 示例,它在缩放比例为 150% 的 4K 屏幕上重现了该问题:
<Window x:Class="Test.Views.FullscreenPolygon"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" Width="3840" Height="2160"
WindowStyle="None" AllowsTransparency="True" Background="Transparent">
<Grid>
<Canvas x:Name="CanvasArea">
<Polygon Points="2888,0 3360,2140 3840,0" Fill="Black"></Polygon>
<Polygon Points="1920,20 1450,2160 2400,2160" Fill="Black"></Polygon>
</Canvas>
</Grid>
</Window>
如您所见,两个多边形(三角形)都经过缩放以适应 window 的 4K 分辨率。 window 本身呈现为 2560x1440,因为显示器缩放比例为 150%。然而,多边形在其外部呈现,部分呈现在我的第二个屏幕上。
编辑2: 感谢 Jeff,使用他的 project.
中的GetScreenScaleFactorNonDpiAware
方法让它工作
我有时需要考虑屏幕缩放,正如 AlwaysLearning 指出的那样,我不得不导入和使用 user32.dll
,因为我也使用 4K 显示器,但我的显示器缩放到 125%。我为此创建了一个单独的 class。
我有一个测试程序可以利用这个 class。这是测试输出:
monitor name| \.\DISPLAY2
native dpi| 96
screen dpi| 120
scale factor| 1.25
scaling factor| 0.8
native screen size| {X=0,Y=0,Width=3840,Height=2160}
scaled screen size| {X=0,Y=0,Width=3072,Height=1728}
以上是从这里创建的:
logMsgLn2("monitor name", ScreenParameters.GetMonitorName(this));
logMsgLn2("native dpi", ScreenParameters.GetNativeScreenDpi);
logMsgLn2("screen dpi", ScreenParameters.GetScreenDpi(this));
logMsgLn2("scale factor", ScreenParameters.GetScreenScaleFactor(this));
logMsgLn2("scaling factor", ScreenParameters.GetScreenScalingFactor(this));
logMsgLn2("native screen size", ScreenParameters.GetNativeScreenSize(this));
logMsgLn2("scaled screen size", ScreenParameters.GetScaledScreenSize(this));
这是全部class:
public class ScreenParameters
{
private const double NativeScreenDpi = 96.0;
private const int CCHDEVICENAME = 32;
// private method to get the handle of the window
// this keeps this class contained / not dependant
public static double GetNativeScreenDpi
{
get => (int) NativeScreenDpi;
}
public static string GetMonitorName(Window win)
{
MONITORINFOEX mi = GetMonitorInfo(GetWindowHandle(win));
return mi.DeviceName;
}
private static IntPtr GetWindowHandle(Window win)
{
return new WindowInteropHelper(win).Handle;
}
// the actual screen DPI adjusted for the scaling factor
public static double GetScreenDpi(Window win)
{
return GetDpiForWindow(GetWindowHandle(win));
}
// this is the ratio of the current screen Dpi
// and the base Dpi
public static double GetScreenScaleFactor(Window win)
{
return (GetScreenDpi(win) / NativeScreenDpi);
}
// this is the conversion factor between screen coordinates
// and sizes and their actual actual coordinate and size
// e.g. for a screen set to 125%, this factor applied
// to the native screen dimensions, will provide the
// actual screen dimensions
public static double GetScreenScalingFactor(Window win)
{
return (1 / (GetScreenDpi(win) / NativeScreenDpi));
}
// get the dimensions of the physical / native screen
// ignoring any applied scaling
public static Rectangle GetNativeScreenSize(Window win)
{
MONITORINFOEX mi = GetMonitorInfo(GetWindowHandle(win));
return ConvertRectToRectangle(mi.rcMonitor);
}
// get the screen dimensions taking the screen scaling into account
public static Rectangle GetScaledScreenSize2(Window win)
{
double ScalingFactor = GetScreenScalingFactor(win);
Rectangle rc = GetNativeScreenSize(win);
if (ScalingFactor == 1) return rc;
return rc.Scale(ScalingFactor);
}
public static Rectangle GetScaledScreenSize(Window win)
{
double dpi = GetScreenDpi(win);
Rectangle rc = GetNativeScreenSize(win);
return ScaleForDpi(rc, dpi);
}
internal static MONITORINFOEX GetMonitorInfo(IntPtr ptr)
{
IntPtr hMonitor = MonitorFromWindow(ptr, 0);
MONITORINFOEX mi = new MONITORINFOEX();
mi.Init();
GetMonitorInfo(hMonitor, ref mi);
return mi;
}
#region + Utility methods
public static Rectangle ConvertRectToRectangle(RECT rc)
{
return new Rectangle(rc.Top, rc.Left,
rc.Right - rc.Left, rc.Bottom - rc.Top);
}
public static System.Drawing.Point ScaleForDpi(System.Drawing.Point pt, double dpi)
{
double factor = NativeScreenDpi / dpi;
return new System.Drawing.Point((int) (pt.X * factor), (int) (pt.Y * factor));
}
public static Point ScaleForDpi(Point pt, double dpi)
{
double factor = NativeScreenDpi / dpi;
return new Point(pt.X * factor, pt.Y * factor);
}
public static Size ScaleForDpi(Size size, double dpi)
{
double factor = NativeScreenDpi / dpi;
return new Size(size.Width * factor, size.Height * factor);
}
public static System.Drawing.Size ScaleForDpi(System.Drawing.Size size, double dpi)
{
double factor = NativeScreenDpi / dpi;
return new System.Drawing.Size((int) (size.Width * factor), (int) (size.Height * factor));
}
public static Rectangle ScaleForDpi(Rectangle rc, double dpi)
{
double factor = NativeScreenDpi / dpi;
return new Rectangle(ScaleForDpi(rc.Location, dpi),
ScaleForDpi(rc.Size, dpi));
}
#endregion
#region + Dll Imports
[DllImport("user32.dll")]
internal static extern UInt16 GetDpiForWindow(IntPtr hwnd);
[DllImport("user32.dll")]
internal static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
internal static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi);
[DllImport("user32.dll")]
internal static extern UInt16 GetProcessDpiAwareness(IntPtr hwnd);
#endregion
#region + Dll Enums
internal enum dwFlags : uint
{
MONITORINFO_PRIMARY = 1
}
#endregion
#region + Dll Structs
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
internal struct MONITORINFOEX
{
public uint cbSize;
public RECT rcMonitor;
public RECT rcWorkArea;
public dwFlags Flags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHDEVICENAME)]
public string DeviceName;
public void Init()
{
this.cbSize = 40 + 2 * CCHDEVICENAME;
this.DeviceName = String.Empty;
}
}
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
#endregion
}
希望对您有所帮助。
为了将来参考,我稍微更新了代码以允许非 DPI 感知监视器。我将更新后的代码放在这里 ScreenParameters