问题
I have a non-DPI aware WPF application where I want to draw a set of polygons in a borderless window to fit exactly on a monitor. I have an algorithm in place to scale and draw my polygons to any given resolution. In my setup I have a 4K and a FullHD monitor next to each other. My 4K monitor has its scale set to 150% and the FullHD monitor is set to 100%. For the 4K monitor, this means that if I set a windows width and height to 3840x2160, the actual rendered resolution is 2560x1440. Now if I scale my set of polygons to 4K, the polygons get rendered outside the canvas and the window. I suspect this is because the polygons are not aware of the DPI setting of my 4K monitor. If I draw the polygons on my FullHD monitor, they fit perfectly since that monitors scale is set to 100%.
To combat this problem, I have tried the following:
- Retrieve the DPI of each monitor and scale the polygons with the DPI in mind.
This works partly. Since my application is non-DPI aware (note that I am not willing to make it DPI aware since that introduces a whole new set of problems), any method for retrieving a monitors DPI results in getting 144 (150%) for both monitors. This results in the polygons fitting perfectly on my 4K monitor, but they will be scaled too small on my FullHD monitor. I have tried the following methods for retrieving DPI: GetDpiForMonitor
from Shcore.dll
, VisualTreeHelper
and Matrixes. Note that these methods do work if I set my application to be DPI aware, but I can not do that for all the extra work that introduces.
- ViewBox wrapping the Canvas
ViewBox does not automatically downscale the contents when I set the canvas width and height to 3840x2160 (ViewBox requires its contents, the canvas, to have a set width and height).
- Retrieving the "real"/scaled resolution of a monitor
With this I mean I need to access an API of some kind which will return a resolution of 2560x1440 for my 4K monitor. I have tried the classic Windows.Forms.Screen
API as well as the newer WindowsDispalyAPI. But both always return a 4K resolution for my 4K monitor.
So all my algorithms are working, I only need to find any of the following:
- A reliable way to retrieve DPI of an individual monitor while keeping my application non-DPI aware.
- A way to retrieve the scaled resolution of monitor.
- Some other way to scale a set of polygons to fit the screen.
Any help is appreciated.
Edit:
Here is an xaml exmaple of a borderless window which reproduces the problem on a 4K screen with 150% scaling:
<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>
As you can see, both polygons (triangles) are scaled to fit the 4K resolution of the window. The window itself gets rendered as 2560x1440, because of the 150% scaling of the monitor. The polygons however, get rendered outside of it, partly onto my second screen.
Edit2:
Got it working thanks to Jeff, using the GetScreenScaleFactorNonDpiAware
method in his project.
回答1:
I needed to account for screen scaling at some point and, as AlwaysLearning notes, I had to import and use user32.dll
as I too use 4K monitors but mine are scaled to 125%. I created a separate class for this.
I have a test program to utilize this class. Here is the test output:
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}
the above is created from this:
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));
Here is the whole 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
}
I hope this helps.
For future reference, I updated the code a bit to allow for Non-DPI aware monitors. I placed the updated code here ScreenParameters
来源:https://stackoverflow.com/questions/57445825/scaling-a-set-of-polygons-to-monitor-resolution-in-wpf